/*************************************************************************
 Copyright © 2000-2003 Novell, Inc. All Rights Reserved.

 THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
 TREATIES. USE AND REDISTRIBUTION OF THIS WORK IS SUBJECT TO THE LICENSE
 AGREEMENT ACCOMPANYING THE SOFTWARE DEVELOPMENT KIT (SDK) THAT CONTAINS
 THIS WORK. PURSUANT TO THE SDK LICENSE AGREEMENT, NOVELL HEREBY GRANTS
 TO DEVELOPER A ROYALTY-FREE, NON-EXCLUSIVE LICENSE TO INCLUDE NOVELL'S
 SAMPLE CODE IN ITS PRODUCT. NOVELL GRANTS DEVELOPER WORLDWIDE DISTRIBUTION
 RIGHTS TO MARKET, DISTRIBUTE, OR SELL NOVELL'S SAMPLE CODE AS A COMPONENT
 OF DEVELOPER'S PRODUCTS. NOVELL SHALL HAVE NO OBLIGATIONS TO DEVELOPER OR
 DEVELOPER'S CUSTOMERS WITH RESPECT TO THIS CODE.
 ************************************************************************/

package com.novell.nds.dirxml.driver.vrtest;


import java.io.*;
import java.net.*;
import java.util.*;

import com.novell.nds.dirxml.driver.*;
import com.novell.nds.dirxml.vrtest.*;
import com.novell.nds.dirxml.driver.xds.*;


/**
 *  A DirXML-friendly <code>VRTestAPI</code> wrapper.  Maps
 *  logical DirXML operations to a corresponding set of
 *  VRTest client API methods.
 */
class VRTestAPIWrapper
        implements VRTestConstants
{


    /** Client id.  Used to detect loopback events. */
    private int id;

    /**
     *  The first of two sequential port numbers used by <code>api</code> to
     *	communicate with the VRTest Server.  The first port number is
     *	used to submit commands.  The second to receive event notifications.
     */
    private int port;

    /**
     *	The host name of the server where the VRTest server process
     *  is running.
     */
    private String host;

    /**
     *	Connection state.  Open or closed.
     */
    private boolean connected;

    /**
     *	Connection state.  Good or bad.
     */
    private boolean bad;

    /**
     *	Used to synchronize calls to
     *	<code>VRTestAPI.startSession(String, int)</code> and
     *	<code>VRTestAPI.closeSession()</code>.
     */
    final public Object connectMutex;

    /** Should loopback events be ingored? */
    private boolean loopBack;

    /**
     *  The VRTest server's schema.  This schema as defined in the
     *  schema definition file "schema.txt".
     */
    private VRTestSchema schema;

    /**	The VRTest client API. */
    private VRTestAPI api;

    /**
     *	A dummy object representing root on the VRTest server.
     */
    private VRTestRootObject root;

    /**
     *  The VRTest publisher.
     */
    private XmlQueryProcessor queryProcessor;

    /**
     *	A warning document published if a VRTest object is missing
     *	an association value.
     */
    private XDSCommandResultDocument associationDoc;


    /**
     *	Constructor.
     *  <p>
     *  @param hostValue the name of the server where the VRTest
     *      server process is running
     *  @param portValue the port number where the VRTest server listener
     *      is waiting for connections
     */
    VRTestAPIWrapper(String hostValue,
                     int portValue
                     )
    {
        XDSStatusElement status;

        bad = true;
        connected = false;
        host = hostValue;
        port = portValue;
        api = new VRTestAPI();
        root = new VRTestRootObject();
        loopBack = false;
        connectMutex = api.connectMutex;
        associationDoc = new XDSCommandResultDocument();
        status = associationDoc.appendStatusElement();
        status.setLevel(StatusLevel.WARNING);
        status.appendText(Errors.NO_ASSOCIATION);
    }//VRTestAPIWrapper(String, int)


    /**
     *  Is a connection open?
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *  @return <code>true</code> or <code>false</code>
     */
    boolean connected()
    {
        return connected;
    }//connected():boolean


    /**
     *  Is the current connection bad?
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *  @return <code>true</code> or <code>false</code>
     */
    boolean badConnection()
    {
        return bad;
    }//badConnection():boolean


    /**
     *	Sets value of <code>queryProcessor</code>.
     *  <p>
     *	NOTE:  Should be called prior to invoking
     *	<code>waitForNotification(XmlCommandProcessor, Trace)</code>.
     *  Not thread safe.
     *  <p>
     *	@param aProcessor the value to set
     *      <code>queryProcessor</code> to
     *	@see #waitForNotification(XmlCommandProcessor, Trace)
     */
    void setQueryProcessor(XmlQueryProcessor aProcessor)
            throws VRTestException
    {
        queryProcessor = aProcessor;
    }//setQueryProcessor(XmlQueryProcessor):void


    /**
     *	Set value of <code>loopBack</code>.
     *  <p>
     *	NOTE:  Should be called prior to invoking
     *	<code>waitForNotification(XmlCommandProcessor, Trace)</code>.
     *  Not thread safe.
     *  <p>
     *	@param allowLoopBack the value to set <code>lookBack</code>
     *      to
     *	@see #waitForNotification(XmlCommandProcessor, Trace)
     */
    public void setLoopBack(boolean allowLoopBack)
    {
        loopBack = allowLoopBack;
    }//setLoopBack(boolean):void


    /**
     *	Creates an object on the VRTest server.
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *	@param className the new object's class name in the VRTest
     *      namespace; must not be <code>null</code>
     *	@param attributes the new object's attributes and attribute values;
     *      must not be <code>null</code>
     *	@return will not return <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public VRTestObject createObject(String className,
                                     VRTestAddAttributes attributes
                                     )
            throws VRTestException,
            IOException
    {
        if (attributes == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("className"));
        }

        VRTestObject object;
        long handle;

        //create checks param validity
        handle = api.create(className, attributes.getList());
        object = new VRTestObject(handle,
                                  null, //object name
                                  className,
                                  this);

        //we'll need the association to complete the
        //	operation, so check to see if we have it
        //FIX:  this should be a runtime exception
        if (object.extractAssociationText() == null)
        {
            throw new VRTestException
                    (Errors.NO_ASSOCIATION);
        }

        return object;
    }//createVRTestObject(String, VRTestAddAttributes):VRTestObject


    /**
     *	Deletes an object on the VRTest server.
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *	@param association the object's association value;
     *      must not be <code>null</code>
     *	@param className the object's class name in the VRTest namespace;
     *      may be <code>null</code>.
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public void deleteObject(String association,
                             String className
                             )
            throws VRTestException, IOException
    {
        VRTestObject object;

        object = getObjectByAssociation(association, className);
        if (object == null)
        {
            //if the object isn't on the server, it's already been deleted so
            //	return successfully
            return;
        }
        else
        {
            api.deleteByHandle(object.getHandle());
        }
    }//deleteObject(String, String):void


    /**
     *	Determines whether <code>object</code> is root.
     *  <p>
     *  NOTE:  Thread safe.
     *  <p>
     *	@param object the object to evaluate
     *	@return <code>true</code> if <code>object</code> is root;
     *	<code>false</code> if it isn't
     */
    public boolean isRootObject(VRTestObject object)
    {
        return (root == object);
    }//isRootObject(VRTestObject):boolean


    /**
     *	Returns an object representing root on the VRTest server.  It
     *  doesn't actually exist.
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *  @return will not return <code>null</code>.
     */
    public VRTestRootObject getRootObject()
    {
        return root;
    }//getRootObject():VRTestRootObject


    /**
     *	Retrieves all objects from the VRTest Server of class
     *	<code>className</code> that have all of the attributes and attribute
     *	values in <code>filter</code>.
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *	@param className the class name in the VRTest namespace;
     *      must not be <code>null</code>.
     *	@param filter the attributes and attribute values a matching object
     *		must have; if <code>filter</code> is
     *      <code>VRTestAttributeList.NO_ATTRIBUTES</code>,
     *		all objects of class <code>className</code> are retrieved
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public List getMatchingObjects(String className,
                                   VRTestAttributeFilter filter
                                   )
            throws VRTestException, IOException
    {
        //className checked in api call

        VRTestObject object;
        VRTestAttributeList attributes;
        long handle;
        List objects;

        if (filter == VRTestAttributeFilter.NO_ATTRIBUTES)
        {
            attributes = null;
        }
        else
        {
            attributes = filter.getList();
        }

        //default size == 10
        objects = new LinkedList();

        try
        {
            synchronized (api.findMutex)
            {
                handle = api.findFirst(className, attributes);

                while (handle != VRTestHandle.NO_MATCH)
                {
                    object = new VRTestObject(handle,
                                              null, //object's name
                                              className,
                                              this);
                    objects.add(object);

                    handle = api.findNext();
                }
            }//synchronized

            return objects;
        }
        catch (IOException io)
        {
            bad = true;
            throw io;
        }
    }//getAllMatchingObjects(String, VRTestAttributeFilter):List


    /**
     *	Modifies <code>object</code>'s attributes by adding or removing
     *	the attribute values in <code>attributes</code>.
     *  <p>
     *	NOTE:  Thread safe.
     *  <p>
     *	@param object the object to be modified on the VRTest server;
     *      must not <code>null</code>
     *	@param attributes the attributes to be modified; must
     *      not be <code>null</code>.
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException  if a communication error occurs
     */
    public void modifyObject(VRTestObject object,
                             VRTestModifyAttributes attributes
                             )
            throws VRTestException, IOException
    {
        if (object == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("object"));
        }

        if (attributes == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("attributes"));
        }

        //ensure the object's attributes are updated
        try
        {
            api.modifyByHandle(object.getHandle(),
                               attributes.getList());
            object.resetAttributes();
        }
        catch (IOException io)
        {
            bad = true;
            throw io;
        }
    }//modifyObject(VRTestObject, VRTest):void


    /**
     *	Returns event notifications.
     *  <p>
     *  NOTE:  Blocks until an event notification is received from
     *  the VRTest server or <code>disconnect()</code>
     *	is called.  Should be called after
     *  <code>setLoopBack(boolean)</code> and
     *  <code>setQueryProcessor(XmlQueryProcessor)</code>.
     *  <p>
     *	@param processor the <code>XmlQueryProcessor</code> used to
     *      publish warnings; must not be <code>null</code>
     *  @param trace an interface to DSTrace; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs or
     *      <code>disconnect()</code> is called
     *	@see #closeConnection()
     *	@see #setLoopBack(boolean)
     *	@see #setQueryProcessor(XmlQueryProcessor)
     */
    public VRTestNotification waitForNotification(XmlCommandProcessor processor,
                                                  Trace trace
                                                  )
            throws VRTestException, IOException
    {
        if (processor == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("processor"));
        }

        VRTestModifiedObject object;
        VRTestEvent event;

        while (true)
        {
            event = null;
            object = null;

            try
            {
                while (event == null)
                {
                    event = api.waitForEvent();

                    if (isLoopBack(event.getID()))
                    {
                        trace.trace("Loopback event.");
                        event = null;
                        continue;
                    }

                    object = new VRTestModifiedObject(event.getHandle(),
                                                      null, //name
                                                      event.getClassName(),
                                                      event.getAttributes(),
                                                      this);

                    if (object.extractAssociationText() == null)
                    {
                        processor.execute(associationDoc.toXML(), queryProcessor);
                        event = null;
                    }
                }//while
            }//try
            catch (IOException io)
            {
                bad = true;
                throw io;
            }

            VRTestNotification notification;
            return new VRTestNotification(event.getType(),
                                          object);
        }//while
    }//waitForNotification(XmlCommandProcessor, Trace):VRTestNotification


    /**
     *	Returns the VRTest server's schema.
     *  <p>
     *  NOTE: Thread safe.
     *  <p>
     *	@return The VRTest server's schema.
     */
    public VRTestSchema getSchema()
    {
        return schema;
    }//getSchema():VRTestSchema


    /**
     *	Open's a connection.
     *  <p>
     *  NOTE:  Not thread safe.  Calls to this method should be
     *      synchronize on <code>connectMutex</code>.
     *  <p>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public void openConnection()
            throws VRTestException, IOException
    {
        if (!connected)
        {
            api.startSession(host, port);
            id = api.getClientID();
            schema = api.getSchema();
            bad = false;
            connected = true;
        }
    }//openConnection():void


    /**
     *	Closes open connection.  If a connection
     *	isn't currently open, no action is taken.
     *  <p>
     *  NOTE:  Not thread safe.  Calls to this method should be
     *      synchronize on <code>connectMutex</code>.
     *  <p>
     */
    public void closeConnection()
    {
        connected = false;
        bad = true;
        api.endSession();
    }//closeConnection():void


    /**
     *	Attempts to close an open connection and open a new one.  If a
     *	connection isn't already open, no action is taken.
     *  <p>
     *  NOTE:  Not thread safe.  Calls to this method should be
     *      synchronize on <code>connectMutex</code>.
     *  <p>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public void resetConnection()
            throws VRTestException, IOException
    {
        closeConnection();
        openConnection();
    }//resetSession():void


    /**
     *	Returns the object on the VRTest server with the association
     *  <code>association</code>.
     *  <p>
     *  NOTE: Thread safe.
     *  <p>
     *  @param association the object's association value;
     *      must not be <code>null</code>
     *  @param className the object's class name in the VRTest
     *      namespace; may be <code>null</code>
     *  @return the matching object or <code>null</code> if no
     *      match is found
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public VRTestObject getObjectByAssociation(String association,
                                               String className
                                               )
            throws VRTestException, IOException
    {
        if (association == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("association"));
        }

        if (root.hasAssociation(association))
        {
            return root;
        }

        VRTestAttributeFilter filter;

        filter = new VRTestAttributeFilter();
        filter.addAttribute(UNIQUE_ID, association);

        try
        {
            return findObject(className, filter);
        }
        catch (IOException io)
        {
            bad = true;
            throw io;
        }
    }//getObjectByAssociation(String, String):VRTestObject


    private VRTestObject findObject(String className,
                                    VRTestAttributeFilter filter
                                    )
            throws VRTestException, IOException
    {
        long handle;

        handle = VRTestHandle.NO_MATCH;
        if (className != null)
        {
            //don't need to search all classes because a class name was
            //	specified
            synchronized (api.findMutex)
            {
                //search the class 'className' for an object that has
                //	a unique id attribute with a value equal to
                //	'association'
                handle = api.findFirst(className, filter.getList());
            }
        }//else
        else
        {
            //search all classes

            ListIterator c;
            VRTestClassSchema classSchema;

            c = schema.getClassSchemas().listIterator();
            //search all calsses for an object that has a unique id
            //	attribute with a value equal to 'association'
            while (c.hasNext())
            {
                classSchema = (VRTestClassSchema) c.next();
                synchronized (api.findMutex)
                {
                    handle = api.findFirst(classSchema.toString(),
                                           filter.getList());
                }

                if (handle != VRTestHandle.NO_MATCH)
                {
                    //found it
                    className = classSchema.toString();
                    break;
                }
            }//while
        }//else

        if (handle == VRTestHandle.NO_MATCH)
        {
            //no object has this association value
            return null;
        }
        else
        {
            return new VRTestObject(handle,
                                    null, //don't have the object's name
                                    className,
                                    this);
        }
    }//findObject(String, VRTestAttributeFilter):VRTestObject


    /**
     *	Returns the object on the VRTest server with the dn
     *  <code>dn</code>.
     *  <p>
     *  NOTE:  Thread safe.
     *  <p>
     *  @param dn the object's dn; must not be <code>null</code>
     *  @return the matching object or <code>null</code> if no
     *      match is found
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    public VRTestObject getObjectByDN(String dn)
            throws VRTestException, IOException
    {
        if (root.hasDN(dn))
        {
            return root;
        }
        else
        {
            VRTestDNParser parser;

            parser = new VRTestDNParser(dn, VRTestDNType.VRTEST);
            try
            {
                return getObjectByNameANDPath(parser.getObjectName(),
                                              parser.getObjectPath());
            }
            catch (IOException io)
            {
                bad = true;
                throw io;
            }
        }//else
    }//getObjectByDN(String):VRTestObject


    /**
     *	Returns the object on the VRTest server with the name
     *  <code>name</code> and path <code>path</code>.
     *  <p>
     *  NOTE:  Thread safe.
     *  <p>
     *  @param name the object's name criterion; must not be <code>null</code>
     *  @param path the object's path criterion; must not be <code>null</code>
     *  @return the matching object or <code>null</code> if no
     *      match is found
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    VRTestObject getObjectByNameANDPath(String name,
                                        String path
                                        )
            throws VRTestException, IOException
    {
        if (name == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("name"));
        }

        if (path == null)
        {
            throw new IllegalArgumentException
                    (Errors.nullParam("path"));
        }

        try
        {
            return findObject(name, path);
        }
        catch (IOException io)
        {
            bad = true;
            throw io;
        }
    }//getObjectByNameANDPath(String, String):VRTestObject


    private VRTestObject findObject(String name,
                                    String path
                                    )
            throws VRTestException, IOException
    {
        ListIterator c;
        VRTestClassSchema classSchema;
        VRTestAttributeSchema namingAttributeSchema;
        VRTestAttributeFilter filter;
        String namingAttributeName;
        long handle;

        c = schema.getClassSchemas().listIterator();
        handle = VRTestHandle.NO_MATCH;
        filter = new VRTestAttributeFilter();
        classSchema = null;
        namingAttributeName = null;

        //search each class for an object with a path attribute
        //	having a value of 'path' and a naming attribute having
        //	a value of 'name'
        while (c.hasNext())
        {
            classSchema = (VRTestClassSchema) c.next();
            namingAttributeSchema = classSchema.getNamingAttributeSchema();
            namingAttributeName = namingAttributeSchema.getName();
            filter.addAttribute(OBJECT_PATH, path);
            filter.addAttribute(namingAttributeName, name);

            synchronized (api.findMutex)
            {
                handle = api.findFirst(classSchema.getName(), filter.getList());
            }

            if (handle != VRTestHandle.NO_MATCH)
            {
                //found it!
                break;
            }

            filter.clear();
        }//while

        if (handle == VRTestHandle.NO_MATCH)
        {
            //no match
            return null;
        }
        else
        {
            return new VRTestObject(handle,
                                    name,
                                    classSchema.getName(), //class name
                                    this);
        }
    }//findObject(String, String):VRTestObject


    /**
     *	Returns the attributes of the object on the VRTest server
     *  with a handle value of <code>handle</code>.
     *  <p>
     *  NOTE:  Thread safe.
     *  <p>
     *  @param handle the object's handle criterion
     *  @return the object's attributes or <code>null</code> if no match
     *      is found.
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    VRTestAttributeList getAttributes(long handle)
            throws VRTestException, IOException
    {
        return api.getAttributesByHandle(handle);
    }//getAttributes(long):VRTestAttributeList


    private boolean isLoopBack(long eventID)
    {
        return ((!loopBack) &&
                (id == eventID));
    }//isLoopBackEvent(String):boolean

}//class VRTestAPIWrapper





