/*************************************************************************
 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.util.*;
import java.net.*;

import org.w3c.dom.*;
import com.novell.nds.dirxml.vrtest.*;
import com.novell.nds.dirxml.driver.*;

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


/**
 *	The VRTest publisher.  Publishes VRTest server events to the
 *  directory.
 */
public class VRTestPublicationShim
        extends CommonImpl
        implements PublicationShim, XmlQueryProcessor, VRTestConstants
{

    //this constant identifies the channel/module being traced
    //  (e.g., VRTest Driver\Publisher)
    static final private String TRACE_SUFFIX = "Publisher";

    //MODIFY:  put the number of publisher parameters your driver uses here
    static private final int NO_OF_PARAMS = 5;

    //parameter tag names:
    static private final String TAG_ALLOW_LOOPBACK = "allow-loopback";
    static private final String TAG_USE_FILTER = "use-filter";
    static private final String TAG_SAVE_STATE = "save-state-each-event";
    static private final String TAG_MODIFY_ALL = "modify-all";

    /** How long to wait between reconnection attempts. */
    static final private int RECONNECT_WAIT_MILLIS = 10000;  //10 seconds


    /**
     *  Variable used to determine when to return from
     *  <code>start()</code>.
     *  <p>
     *  @see #start(XmlCommandProcessor)
     */
    private boolean running;


    /**
     *  Used to filter VRTest server events before sending them to
     *  the DirXML engine.
     *  <p>
     *  @see #start(XmlCommandProcessor)
     */
    private VRTestDriverFilter filter;

    /**
     *  Container for publisher parameters from the document passed to
     *  <code>init(XmlDocument)</code>.
     *  <p>
     *  @see #init(XmlDocument)
     *  @see #setPubParams()
     */
    private Map pubParams;

    /**
     *  A reference to this driver instance.
     *  Used to retrieve configuration information.
     *  <p>
     *  @see VRTestDriverShim#getDriverRDN()
     *  @see VRTestDriverShim#getDriverParams()
     */
    private VRTestDriverShim driver;

    /**  This publisher's thread. */
    private Thread thread;

    /** Publish loopback events? */
    private boolean allowLoopBack;

    /** Filter publication events? */
    private boolean useFilter;

    /** Add state info to each event? */
    private boolean saveState;

    /** Resync object with each modification? */
    private boolean modifyAll;


    /**
     *  Constructor.
     *  <p>
     *  @param someDriver a reference to this driver instance;
     *      must not be <code>null</code>
     */
    public VRTestPublicationShim(VRTestDriverShim someDriver)
    {
        driver = someDriver;

        setAPI(driver.getAPI());
        setDriverRDN(driver.getDriverRDN());
        setDriverParams(driver.getDriverParams());
        setTrace(TRACE_SUFFIX);
        setPubParams();

        running = false;
        thread = null;
    }//VRTestPublicationShim(VRTestDriverShim)


    /**
     *  A non-interface method that describes the parameters
     *  this PublicationShim is expecting.
     *  <p>
     *  @see VRTestPublicationShim#init(XmlDocument)
     */
    private void setPubParams()
    {
        Parameter param;

        pubParams = new HashMap(NO_OF_PARAMS);

        param = new Parameter(TAG_ALLOW_LOOPBACK, //tag name
                              "no", //default value
                              DataType.BOOLEAN); //data type
        pubParams.put(param.tagName(), param);

        param = new Parameter(TAG_USE_FILTER, //tag name
                              "yes", //default value
                              DataType.BOOLEAN); //data type
        pubParams.put(param.tagName(), param);

        param = new Parameter(TAG_SAVE_STATE, //tag name
                              "no", //default value
                              DataType.BOOLEAN); //data type
        pubParams.put(param.tagName(), param);

        param = new Parameter(TAG_MODIFY_ALL, //tag name
                              "no", //default value
                              DataType.BOOLEAN); //data type
        pubParams.put(param.tagName(), param);

        //state parameters; must be optional since not available when driver
        //  first started
        param = new Parameter(TAG_RUN_COUNT, //tag name
                              DEFAULT_COUNT, //default
                              DataType.LONG); //data type
        param.add(RangeConstraint.POSITIVE);
        pubParams.put(param.tagName(), param);
    }//setPubParams():void


    /**
     *  Initializes this publisher.
     *  <p>
     *  @param initXML XML document that contains the publisher
     *      initialization parameters and state
     *  @return an XML document containing status messages for this
     *      operation
     */
    public XmlDocument init(XmlDocument initXML)
    {
        trace.trace("init", 1);

        XDSResultDocument result;
        StatusAttributes attrs;

        //create result document for reporting status to the DirXML engine
        result = newResultDoc();

        try
        {
            XDSInitDocument init;
            XDSStatusElement status;
            Parameter param;

            //parse initialization document
            init = new XDSInitDocument(initXML);

            //get publisher parameters/state from init doc
            init.parameters(pubParams);

            param = (Parameter) this.pubParams.get(TAG_RUN_COUNT);
            setRunCount(param.toLong().longValue());
            param.overrideValue(String.valueOf(getRunCount()));
            this.allowLoopBack = ((Parameter) this.pubParams.get(TAG_ALLOW_LOOPBACK)).toBoolean().booleanValue();
            this.useFilter = ((Parameter) this.pubParams.get(TAG_USE_FILTER)).toBoolean().booleanValue();
            this.saveState = ((Parameter) this.pubParams.get(TAG_SAVE_STATE)).toBoolean().booleanValue();
            this.modifyAll = ((Parameter) this.pubParams.get(TAG_MODIFY_ALL)).toBoolean().booleanValue();

            filter = new VRTestDriverFilter(init.extractInitParamsElement().extractDriverFilterElement().domElement());

            //append publisher state info to result doc
            appendStateInfo(result.appendInitParamsElement().appendPublisherStateElement());

            //append a successful <status> element to the result doc
            attrs = StatusAttributes.factory(StatusLevel.SUCCESS,
                                             StatusType.DRIVER_STATUS,
                                             null); //event-id
            status = XDSUtil.appendStatus(result, //doc to append to
                                          attrs,
                                          null); //description
            //append the parameter values the publisher is actually using
            status.parametersAppend(pubParams);
        }//try
        catch (Exception e) //don't want to catch Error class with Throwable
        {
            //e instance of XDSException:
            //
            //  init document is malformed or invalid -- or --
            //  it is missing required parameters or contains
            //  illegal parameter values

            //e instance of RuntimeException:
            //
            //  e.g., NullPointerException

            attrs = StatusAttributes.factory(StatusLevel.FATAL,
                                             StatusType.DRIVER_STATUS,
                                             null); //event-id
            XDSUtil.appendStatus(result, //doc to append to
                                 attrs, //status attribute values
                                 null, //description
                                 e, //exception
                                 XDSUtil.appendStackTrace(e), //append stack trace?
                                 initXML); //xml to append
        }//catch

        //return result doc w/ status to DirXML
        return result.toXML();
    }//init(XmlDocument):XmlDocument


    /**
     *	Starts this publisher.
     *  <p>
     *  NOTE:  Entry point for publisher thread.
     *      Should not return until <code>shutdown()</code> is called.
     *  <p>
     *  @param processor <code>XmlCommandProcessor</code> that can invoked in
     *      order to publish information to eDirectory on behalf of the application;
     *      processor must only be invoked from the thread on which
     *      <code>start()</code> was invoked
     *  @return XML document containing status from start operation
     *	@see VRTestDriverShim#shutdown(XmlDocument)
     */
    public XmlDocument start(XmlCommandProcessor processor)
    {
        XDSResultDocument resultDoc;
        XDSStatusElement status;

        this.thread = Thread.currentThread();
        resultDoc = newResultDoc();
        status = resultDoc.appendStatusElement();

        try
        {
            setupInitialConnection(processor, status);
            if (this.connected)
            {
                this.running = true;
                publish(processor);

                status.setLevel(StatusLevel.SUCCESS);
                status.setType(StatusType.DRIVER_STATUS);
            }//if
            else
            {
                //status already set
            }
        }//try
        catch (InterruptedException ie)
        {
            status.setLevel(StatusLevel.SUCCESS);
            status.setType(StatusType.DRIVER_STATUS);
        }
        catch (Exception e)
        {
            status.setLevel(StatusLevel.FATAL);
            status.setType(StatusType.DRIVER_STATUS);
            status.descriptionAppend(e.getMessage());
            status.exceptionAppend(e, true);
        }
        finally
        {
            disconnect();
        }

        //return result doc w/ status to DirXML engine
        return resultDoc.toXML();
    }//start(XmlCommandProcessor):XmlDocument


    /**
     *  A non-interface method that passes this publisher's configuraiton
     *  information to <code>api</code>.
     */
    private void setPublicationProperties()
            throws VRTestException
    {
        filter.setProperties(super.api.getSchema());
        super.api.setQueryProcessor(this);
        super.api.setLoopBack(this.allowLoopBack);
    }//setPublicationProperties():void


    /**
     *  Connects to the VRTest server for the first time.
     *  <p>
     *  @param processor the <code>XmlCommandProcessor</code>
     *      used to publish events; must not be <code>null</code>
     *  @param status a reference to the caller's document's
     *      <code>&ltstatus&gt</code> element; must not be <code>null</code>
     *  @throws InterruptedException when <code>shutdown()</code> is called
     *  @see #start(XmlCommandProcessor)
     */
    private void setupInitialConnection(XmlCommandProcessor processor,
                                        XDSStatusElement status
                                        )
            throws InterruptedException, Exception
    {
        int retries;

        retries = 0;
        while (!connected)
        {
            try
            {
                connect();
                setPublicationProperties();
                connected = true;

                if (retries > 0)
                {
                    XDSCommandDocument notifyDoc;

                    notifyDoc = newCommandDoc();
                    status = notifyDoc.appendStatusElement();
                    status.setLevel(StatusLevel.SUCCESS);
                    status.setType(StatusType.APP_CONNECTION);
                    status.appendText("Connected.");
                    processor.execute(notifyDoc.toXML(), this);
                }
                break;
            }//try
            catch (Exception e)
            {
                if (e instanceof VRTestException)
                {
                    status.setLevel(StatusLevel.FATAL);
                    status.setType(StatusType.DRIVER_STATUS);
                    status.descriptionAppend(e.getMessage());
                    status.exceptionAppend(e, true);
                    break;
                }
                else if (e instanceof IOException)
                {
                    XDSCommandDocument notifyDoc;

                    notifyDoc = new XDSCommandDocument();
                    status = notifyDoc.appendStatusElement();
                    status.setLevel(StatusLevel.WARNING);
                    status.setType(StatusType.APP_CONNECTION);
                    status.descriptionAppend("Unable to connect.");
                    status.exceptionAppend(e, false);
                    processor.execute(notifyDoc.toXML(), this);
                    Thread.sleep(RECONNECT_WAIT_MILLIS);
                    retries++;
                }//else if
                else
                {
                    //fatal; get out of while loop and return
                    throw e;
                }
            }//catch
        }//while
    }//setupInitialConnection(XmlCommandProcessor, XDSStatusElement):void


    /**
     *  Publishes event from the VRTest server to the directory.
     *  <p>
     *  @param processor the <code>XmlCommandProcessor</code> used to
     *      publish events; must not be <code>null</code>
     *  @throws InterruptedException when <code>shutdown()</code> is called
     *  @see #shutdown()
     */
    private void publish(XmlCommandProcessor processor)
            throws InterruptedException, Exception
    {
        while (running)
        {
            XDSCommandDocument commandDoc;
            VRTestNotification notification;
            StatusAttributes attrs;

            commandDoc = newCommandDoc();

            try
            {
                if (!connected)
                {
                    connect();
                    setPublicationProperties();
                    connected = true;

                    attrs = StatusAttributes.factory(StatusLevel.SUCCESS,
                                                     StatusType.APP_CONNECTION,
                                                     null);
                    XDSUtil.appendStatus(commandDoc, attrs, "Connected.");
                    processor.execute(commandDoc.toXML(), this);
                    commandDoc = newCommandDoc();
                }//if

                notification = api.waitForNotification(processor,
                                                       trace);
                publishNotification(notification,
                                    processor,
                                    commandDoc);
            }//try
            catch (Exception e)
            {
                commandDoc.empty();
                attrs = StatusAttributes.factory(StatusLevel.ERROR,
                                                 StatusType.APP_GENERAL,
                                                 null);

                if (e instanceof VRTestException)
                {
                    XDSUtil.appendStatus(commandDoc,
                                         attrs,
                                         null,
                                         e,
                                         false,
                                         null);
                    processor.execute(commandDoc.toXML(), this);
                }//if
                else if (e instanceof IOException)
                {
                    if (this.running)
                    {
                        attrs.setLevel(StatusLevel.WARNING);
                        attrs.setType(StatusType.APP_CONNECTION);
                        XDSUtil.appendStatus(commandDoc,
                                             attrs,
                                             getConnectHeader(),
                                             e,
                                             false,
                                             null);
                        processor.execute(commandDoc.toXML(), this);
                        Thread.sleep(RECONNECT_WAIT_MILLIS);
                    }//if
                    else
                    {
                        //normal shutdown
                    }

                    connected = false;
                }//else if
                else
                {
                    //fatal; get out of while loop and return
                    throw e;
                }
            }//catch
        }//while
    }//publish(XmlCommandProcessor):void


    /**
     *  Publishes one event to the directory.
     *  <p>
     *  @param notification the event to published;
     *      must not be <code>null</code>
     *  @param processor the <code>XmlCommandProcessor</code> used
     *      to publish <code>notifyDoc</code>;
     *      must not be <code>null</code>
     *  @param notifyDoc the document to publish;
     *      must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communications error occurs
     */
    private void publishNotification(VRTestNotification notification,
                                     XmlCommandProcessor processor,
                                     XDSCommandDocument notifyDoc
                                     )
            throws VRTestException, IOException
    {
        //this method distinguishes between move, modify, or rename
        //	notifications from the test server based upon the notifications
        //	type

        VRTestModifiedObject object;
        VRTestEventType type;
        VRTestClassFilter classFilter;
        String className;

        object = notification.getObject();
        className = object.getClassName();
        classFilter = filter.getClassFilter(className);
        if ((classFilter == null) &&
                this.useFilter)
        {
            trace.trace("Filtered Class:  " + className);
        }
        else
        {
            if (classFilter != null)
            {
                className = classFilter.getClassName().toString();
            }

            type = notification.getType();
            if (type == VRTestEventType.CREATE)
            {
                appendAddElement(notifyDoc,
                                 object,
                                 classFilter,
                                 className);
            }
            else if (type == VRTestEventType.MODIFY)
            {
                appendMoveModifyORRenameElement(notifyDoc,
                                                object,
                                                classFilter,
                                                className);
            }
            else if (type == VRTestEventType.DELETE)
            {
                appendDeleteElement(notifyDoc,
                                    object,
                                    className);
            }
            else
            {
                XDSStatusElement status;

                status = notifyDoc.appendStatusElement();
                status.setLevel(StatusLevel.WARNING);
                status.appendText(Errors.unsupportedEventType(type.toString()));
            }

            publishNotificationDocument(notifyDoc, processor);
        }//else
    }//publishNotification(VRTestNotification, XmlCommandProcessor,
    //                    XDSCommandDocument):void


    /**
     *  A non-interface that appends one <code>&lt;add&gt;</code> element to
     *  <code>notifyDoc</code>.<p>
     *  <p>
     *  @param notifyDoc the document to append the <code>&lt;add&gt;</code>
     *      element to; must not be <code>null</code>
     *  @param object the object that was added on the VRTest server;
     *      must not be <code>null</code>
     *  @param classFilter the filter used to filter <code>object</code>'s
     *      attributes; must not be <code>null</code>
     *  @param className <code>object</code>'s class name in the VRTest
     *      namespace; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendAddElement(XDSCommandDocument notifyDoc,
                                  VRTestModifiedObject object,
                                  VRTestClassFilter classFilter,
                                  String className
                                  )
            throws VRTestException, IOException
    {
        //  <!-- the element we're handling -->
        //
        //	<add src-dn="dn" class-name="class">
        //		<association>association</association>
        //		<add-attr/> <!-- * -->
        //	</add>
        //
        //  <!-- * == zero or more -->
        //	<!-- association == required -->

        String association;
        XDSAddElement add;

        add = notifyDoc.appendAddElement();
        add.setClassName(className);
        add.setSrcDN(object.getDN());

        association = object.extractAssociationText();
        add.appendAssociationElement(association);

        appendAddAttrElements(add,
                              object,
                              classFilter,
                              className);
    }//appendAddElement(XDSCommandDocument, VRTestModifiedObject,
     //                 VRTestClassFilter, String):void


    /**
     *  A non-interface method that appends one <code>&lt;move&gt;</code>,
     *  one <code>&lt;modify&gt;</code>, or one <code>&lt;rename&gt;</code>
     *  element to <code>notifyDoc</code>.  Which element(s) are appended
     *  depends upon the nature of the modifications to <code>object</code>.
     *  <p>
     *  @param notifyDoc the document to append element(s) to;
     *      must not be <code>null</code>
     *  @param object the object that was moved, modified, or renamed on
     *      the VRTest server; must not be <code>null</code>
     *  @param classFilter the filter used to filter <code>object</code>'s
     *      attributes; may be <code>null</code>
     *  @param className <code>object</code>'s class name in the
     *      VRTest namespace; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendMoveModifyORRenameElement(XDSCommandDocument notifyDoc,
                                                 VRTestModifiedObject object,
                                                 VRTestClassFilter classFilter,
                                                 String className
                                                 )
            throws VRTestException, IOException
    {
        //is it a rename notification?
        LinkedList newNames;
        String newPath;
        boolean namingAttributeInFilter;
        String namingAttributeName;

        namingAttributeInFilter = true;
        namingAttributeName = object.getNamingAttributeSchema().toString();
        if (classFilter != null)
        {
            namingAttributeName = classFilter.getFilterName(namingAttributeName);
            if (this.useFilter &&
                    (namingAttributeName == null))
            {
                namingAttributeInFilter = false;
            }
        }//else

        newNames = new LinkedList();
        if (namingAttributeInFilter &&
                object.renamed(newNames))
        {
            if (object.getNamingAttributeSchema().isMultiValued())
            {
                appendRenameElement(notifyDoc,
                                    object,
                                    className,
                                    object.getName()); //first value
                if ((newNames.size() != 0) &&
                        (!this.modifyAll))
                {
                    //remove and an add
                    appendOtherNames(notifyDoc,
                                     object,
                                     className,
                                     namingAttributeName);
                    object.removeCurrent();
                }
            }//if
            else
            {
                appendRenameElement(notifyDoc,
                                    object,
                                    className,
                                    (String) newNames.getFirst());
                object.removeCurrent();
            }
        }//if

        newPath = object.getNewPath();
        if (newPath != null)
        {
            appendMoveElement(notifyDoc,
                              object,
                              className,
                              newPath);
        }

        if (object.hasMoreModifiedAttributes())
        {
            appendModifyElement(notifyDoc,
                                object,
                                classFilter,
                                className);
        }
    }//appendMoveModifyORRenameElement(XDSCommandDocument, VRTestModifiedObject,
     //                                VRTestClassFilter, String):void


    /**
     *  A non-interface method that appends <code>object</code>'s
     *  non-naming names to <code>notifyDoc</code>.
     *  <p>
     *  @param notifyDoc the document to append the non-naming
     *      names to; must not be <code>null</code>
     *  @param object the object in question; must not be <code>null</code>
     *  @param className <code>object</code>'s class name in the
     *      VRTest namespace; must not be <code>null</code>
     *  @param namingAttributeName the name of <code>object</code>'s
     *      naming attribute in the VRTest namespace; must not be
     *      <code>null</code>
     */
    private void appendOtherNames(XDSCommandDocument notifyDoc,
                                  VRTestObject object,
                                  String className,
                                  String namingAttributeName
                                  )
            throws IOException, VRTestException
    {
        LinkedList otherNames;
        XDSModifyElement modify;
        XDSModifyAttrElement modifyAttr;

        otherNames = object.getOtherNames();
        if (otherNames != null)
        {
            ListIterator iterator;
            XDSAddValueElement addValue;

            modify = notifyDoc.appendModifyElement();
            modify.setClassName(className);
            modify.setSrcDN(object.getDN());
            modify.appendAssociationElement(object.extractAssociationText());
            modifyAttr = modify.appendModifyAttrElement();
            modifyAttr.setAttrName(namingAttributeName);
            modifyAttr.appendRemoveAllValuesElement();
            addValue = modifyAttr.appendAddValueElement();
            iterator = otherNames.listIterator();
            while (iterator.hasNext())
            {
                addValue.appendValueElement((String) iterator.next());
            }
        }//if
    }//appendOtherNames(XDSCommandDocument, VRTestObject,
     //                 String, String):void


    /**
     *  A non-interface method that appends one <code>&lt;delete&gt;</code>
     *  element to <code>notifyDoc</code>.
     *  <p>
     *  @param notifyDoc the document to append the <code>&lt;delete&gt;</code>
     *      element to; must not be <code>null</code>
     *  @param object the object that was deleted on the VRTest server;
     *      must not be <code>null</code>
     *  @param className <code>object</code>'s class name in the VRTest
     *      namespace; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendDeleteElement(XDSCommandDocument notifyDoc,
                                     VRTestModifiedObject object,
                                     String className
                                     )
            throws VRTestException, IOException
    {
        //  <!-- the element we're constructing -->
        //
        //	<delete class-name="class" src-dn="dn">
        //		<association>assoc</association>
        //	</delete>

        XDSDeleteElement delete;

        delete = notifyDoc.appendDeleteElement();
        delete.setClassName(className);
        //DN is no longer available since object was deleted
        //delete.setSrcDN(object.getDN());
        delete.appendAssociationElement(object.extractAssociationText());
    }//appendDeleteElement(XDSCommandDocument, VRTestModifiedObject,
     //                    String):void


    /**
     *  A non-interface method that appends a <code>&lt;modify&gt;</code> element
     *  to <code>notifyDoc</code>.
     *  <p>
     *  @param notifyDoc the document to append the element to;
     *      must not be <code>null</code>
     *  @param object the object being modified; must not be <code>null</code>
     *  @param classFilter the filter for this object;
     *      must not be <code>null</code>
     *  @param className the object's class name in the VRTest namespace;
     *      must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendModifyElement(XDSCommandDocument notifyDoc,
                                     VRTestModifiedObject object,
                                     VRTestClassFilter classFilter,
                                     String className
                                     )
            throws VRTestException, IOException
    {
        //  <!-- the element we're constructing -->
        //
        //	<modify src-dn="dn" class-name="class">
        //		<association>association</association>
        //		<modify-attr attr-name="name">+
        //	</modify>
        //
        //	<!-- + == one or more -->

        XDSModifyElement modify;

        //append the <modify-attr> element
        modify = notifyDoc.appendModifyElement();
        modify.setClassName(className);
        modify.setSrcDN(object.getDN());
        //append the <association> child element
        modify.appendAssociationElement(object.extractAssociationText());

        //append one or more <modify-attr> child elements
        appendModifyAttrElements(modify,
                                 object,
                                 classFilter);
    }//appendModifyElement(XDSCommandDocument, VRTestModifiedObject object,
     //                    VRTestClassFilter, String):void


    /**
     *  A non-interface method that appends <code>&lt;modify-attr&gt;</code>
     *  element(s) to <code>modify</code>.
     *  <p>
     *  @param modify the element to append to; must not be <code>null</code>
     *  @param object the modified object; must not be <code>null</code>
     *  @param classFilter the filter for this object;
     *      may be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendModifyAttrElements(XDSModifyElement modify,
                                          VRTestModifiedObject object,
                                          VRTestClassFilter classFilter //may be null if not filtering
                                          )
            throws VRTestException, IOException
    {
        //  <!-- the element we're constructing -->
        //
        //	<modify-attr attr-name="name">
        //		<value> <!-- + -->
        //	<modify-attr/>
        //
        //  <!-- + == one or more -->
        //	<!-- attr-name == required -->

        List attributes;
        ListIterator a;
        VRTestClassSchema classSchema;

        if (this.modifyAll)
        {
            attributes = object.getAttributes();
        }
        else
        {
            attributes = object.getModifiedAttributes();
        }

        VRTestAttributeSchema attributeSchema;
        XDSModifyAttrElement modifyAttr;
        VRTestAttribute attribute;
        HashMap attributeNameValueMap;
        LinkedList attributeValues;
        String attrName, attrFilterName;

        attributeNameValueMap = null;
        classSchema = object.getClassSchema();
        a = attributes.listIterator();
        while (a.hasNext())
        {
            attribute = (VRTestAttribute) a.next();
            attrName = attribute.getName();
            attrFilterName = null;

            if (this.useFilter)
            {
                attrFilterName = classFilter.getFilterName(attrName);
                if (attrFilterName == null)
                {
                    trace.trace("Filtered Attribute:  " + attrName);
                    continue;
                }
            }//if

            if (attrFilterName != null)
            {
                //if a schema mapping style sheet is used, matching
                //  may be case-sensitive, so it's best to use the
                //  schema-mapped name as supplied in the filter
                //  whenever possible
                attrName = attrFilterName;
            }

            attributeSchema = classSchema.getAttributeSchema(attrName);
            if (attributeSchema == null)
            {
                throw new VRTestException
                        (Errors.noAttributeSchema(attrName));
            }

            if (this.modifyAll)
            {
                if (attributeNameValueMap == null)
                {
                    attributeNameValueMap = new HashMap();
                }

                attributeValues = (LinkedList) attributeNameValueMap.get(attrName);
                if (attributeValues == null)
                {
                    attributeValues = new LinkedList();
                    attributeNameValueMap.put(attrName, attributeValues);
                }
                attributeValues.add(attribute.getValue());
            }//if
            else
            {
                modifyAttr = modify.appendModifyAttrElement();
                modifyAttr.setAttrName(attrName);
                appendValueElement(modifyAttr, attribute, attributeSchema);
            }
        }//while

        if (this.modifyAll &&
                (attributeNameValueMap != null))
        {
            XDSAddValueElement addValue;
            Iterator keys;
            ListIterator values;
            String attributeName, value;

            keys = attributeNameValueMap.keySet().iterator();
            while (keys.hasNext())
            {
                attributeName = (String) keys.next();
                attributeValues = (LinkedList) attributeNameValueMap.get(attributeName);
                values = attributeValues.listIterator();
                modifyAttr = modify.appendModifyAttrElement();
                modifyAttr.setAttrName(attributeName);
                modifyAttr.appendRemoveAllValuesElement();
                addValue = modifyAttr.appendAddValueElement();
                while (values.hasNext())
                {
                    value = (String) values.next();
                    addValue.appendValueElement(value);
                }
            }//while
        }//if
    }//appendModifyAttrElements(XDSModifyElement, VRTestModifiedObject,
     //                         VRTestClassFilter):void


    /**
     *  A non-interface method that appends <code>&lt;value&gt;</code>
     *  element(s) to <code>modifyAttr</code>.
     *  <p>
     *  @param modifyAttr the element to append to;
     *      must not be <code>null</code>
     *  @param attribute the attribute being modified;
     *      must not be <code>null</code>
     *  @param attributeSchema the schema definition for
     *      <code>attribute</code>; must not be <code>null</code>
     */
    private void appendValueElement(XDSModifyAttrElement modifyAttr,
                                    VRTestAttribute attribute,
                                    VRTestAttributeSchema attributeSchema
                                    )
    {
        //  <!-- the element we're constructing -->
        //
        //	<modify-attr attr-name="name">
        //		<value> <!-- + -->
        //	<modify-attr/>
        //
        //	<!-- + == one or more -->
        //	<!-- attr-name == required -->

        boolean isAdd;

        isAdd = attribute.hasAction(VRTestAction.ADD);
        if (isAdd &&
                !attributeSchema.isMultiValued()) //single-valued
        {
            //vrtest simulates a typical application where we don't know what
            //	the old attribute value was, so we can't do a selective remove
            //debug.comment("Single-valued attribute--doing remove all...");
            modifyAttr.appendRemoveAllValuesElement();
        }

        if (isAdd)
        {
            XDSAddValueElement addValue;

            addValue = modifyAttr.appendAddValueElement();
            addValue.appendValueElement(attribute.getValue());
        }
        else //remove
        {
            XDSRemoveValueElement removeValue;

            removeValue = modifyAttr.appendRemoveValueElement();
            removeValue.appendValueElement(attribute.getValue());
        }
    }//appendValueElement(XDSModifyAttrElement, VRTestAttribute,
     //                   VRTestAttributeSchema):void


    /**
     *  A non-interface method that appends a <code>&lt;rename&gt;</code>
     *  element to <code>notifyDoc</code>.
     *  <p>
     *  @param notifyDoc the document to append the element to;
     *      must not be <code>null</code>
     *  @param object the object being renamed; must not be <code>null</code>
     *  @param className the object's class name; must not be <code>null</code>
     *  @param newName the object's new name; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendRenameElement(XDSCommandDocument notifyDoc,
                                     VRTestModifiedObject object,
                                     String className,
                                     String newName
                                     )
            throws VRTestException, IOException
    {
        //  <!-- the element we're constructing -->
        //
        //	<rename class-name="class" src-dn="dn">
        //		<association>assoc</association>
        //		<new-name>name</new-name>
        //	</rename>

        XDSRenameElement rename;

        rename = notifyDoc.appendRenameElement();
        rename.setClassName(className);
        rename.setRemoveOldName(true);
        rename.setSrcDN(object.getDN());
        rename.appendAssociationElement(object.extractAssociationText());
        rename.appendNewNameElement(newName);
    }//appendRenameElement(XDSCommandDocument, VRTestModifiedObject object,
     //                    String, String:void


    /**
     *  A non-interface method that appends a <code>&lt;move&gt;</code>
     *  element to <code>notifyDoc</code>.
     *  <p>
     *  @param notifyDoc the document to append the element to;
     *      must not be <code>null</code>
     *  @param object the object being moved; must not be <code>null</code>
     *  @param className the object's class name in the VRTest namespace;
     *      must not be <code>null</code>
     *  @param path the object's new path; must not be <code>null</code>
     *	@throws	VRTestException if an API error occurs
     *  @throws IOException if a communication error occurs
     */
    private void appendMoveElement(XDSCommandDocument notifyDoc,
                                   VRTestModifiedObject object,
                                   String className,
                                   String path
                                   )
            throws VRTestException, IOException
    {
        //  <!-- the element we're constructing -->
        //
        //	<move class-name="name" src-dn="dn">
        //		<association>assoc</association>
        //		<parent src-dn="dn">
        //			<association>assoc</association>
        //		</parent>
        //	</move>

        XDSMoveElement move;
        VRTestPathParser parser;
        XDSParentElement parent;
        String containerName;
        String association;
        VRTestRootObject rootObject;

        move = notifyDoc.appendMoveElement();
        move.setClassName(className);
        move.setSrcDN(object.getDN());
        move.appendAssociationElement(object.extractAssociationText());

        //get the parent's dn and association so we can create the remaining
        //	<parent> and <association> elements

        parser = new VRTestPathParser(path);
        parent = move.appendParentElement();
        parent.setSrcDN(parser.getContainerDN());

        //get the parent's association value
        rootObject = api.getRootObject();
        containerName = parser.getContainerName();
        if (rootObject.hasName(containerName))
        {
            //the parent is the root object
            association = rootObject.extractAssociationText();
        }
        else
        {
            //the parent is a child of root
            VRTestObject container;
            container = api.getObjectByNameANDPath(containerName,
                                                   parser.getContainerPath());
            association = container.extractAssociationText();
        }

        //append an <association> element to <parent>
        parent.appendAssociationElement(association);
    }//appendMoveElement(XDSCommandDocument, VRTestModifiedObject,
     //                  String, String):void


    /**
     *	Queries the VRTest server for objects matching the query criteria
     *  specified in <code>queryXML</code>.
     *  <p>
     *  @param queryXML a document containing an XDS-encoded query
     *  @return the results of the query
     */
    public XmlDocument query(XmlDocument queryXML)
    {
        trace.trace("query", 1);

        XDSQueryResultDocument result;
        StatusAttributes attrs;
        String eventID;

        result = new XDSQueryResultDocument();
        appendSourceInfo(result);
        eventID = null;

        try
        {
            XDSQueryDocument queryDoc;
            XDSQueryElement query;
            ListIterator q;

            queryDoc = new XDSQueryDocument(queryXML);
            q = queryDoc.extractQueryElements().listIterator();
            while (q.hasNext())
            {
                query = (XDSQueryElement) q.next();
                eventID = query.getEventID();
                queryHandler(query, result);
                attrs = StatusAttributes.factory(StatusLevel.SUCCESS,
                                                 StatusType.DRIVER_GENERAL,
                                                 eventID);
                XDSUtil.appendStatus(result, //doc to append to
                                     attrs, //status attribute values
                                     null); //description
            }//while
        }//try
        catch (XDSException xds)
        {
            //query doc is malformed or invalid

            attrs = StatusAttributes.factory(StatusLevel.ERROR,
                                             StatusType.DRIVER_GENERAL,
                                             null); //event-id
            XDSUtil.appendStatus(result, //doc to append to
                                 attrs, //status attribute values
                                 null, //description
                                 xds, //exception
                                 false, //append stack trace?
                                 queryXML); //xml to append
        }//try
        catch (Exception e) //don't want to catch Error class with Throwable
        {
            //the DirXML engine ignores "retry" and "fatal" status
            //  messages from this call; everything is basically treated
            //  as an "error."

            if (e instanceof IOException)
            {
                //although not retried, we know the connection is bad
                connected = false;
            }

            //remove previous results
            result.empty(); //FIX:  should make more granular?
                            //  perhaps by event-id?
            attrs = StatusAttributes.factory(StatusLevel.ERROR,
                                             StatusType.DRIVER_GENERAL,
                                             eventID); //event-id
            XDSUtil.appendStatus(result, //doc to append to
                                 attrs, //status attribute values
                                 null, //description
                                 e, //exception
                                 true, //append stack trace?
                                 queryXML); //xml to append
        }//catch

        //return result doc w/ status and query results
        //  to the DirXML engine
        return result.toXML();
    }//query(XmlDocument):XmlDocument


    /**
     *  Stops this publisher.
     *  <p>
     *  NOTE: This method is called by a thread other than the publisher
     *  thread.
     *  <p>
     *  @see VRTestDriverShim#shutdown(XmlDocument)
     */
    void shutdown()
    {
        this.running = false;
        this.thread.interrupt();  //causes publisher thread to stop sleeping
        super.api.closeConnection();
    }//shutdown():void


    /**
     *  Publishes a notification document.
     *  <p>
     *  @param commandDoc the document to be published;
     *      must not be <code>null</code>
     *  @param processor The command processor used to
     *      publish <code>commandDoc</code>.
     */
    private void publishNotificationDocument(XDSCommandDocument commandDoc,
                                             XmlCommandProcessor processor
                                             )
    {
        //only publish doc if the input element has children
        if (!commandDoc.isEmpty())
        {
            if (this.saveState)
            {
                appendStateInfo(commandDoc.appendInitParamsElement().appendPublisherStateElement());
            }

            processor.execute(commandDoc.toXML(),
                              this);
            //we don't need to check the result since there's nothing we can
            //	do in the event of an error
        }
    }//publishNotificationDocument(XDSCommandDocument, XmlCommandProcessor):void


    private void appendAddAttrElements(XDSAddElement add,
                                       VRTestModifiedObject object,
                                       VRTestClassFilter classFilter,
                                       String className
                                       )
    {
        //this method translates each attribute of a newly-created vrtest
        //	object into its its corresponding xds representation
        //	(i.e. an <add-attr> element); if a filter is provided, only the
        //	attributes in the filter will be translated; if successful,
        //	<add-attr> elements are appended to the <add> element;
        //	otherwise, an exception is thrown

        //the elements we're constructing:
        //
        //	<add-attr attr-name="name">
        //		<value>+
        //	<add-attr/>
        //
        //			+ == one or more
        //	attr-name == required

        ListIterator a;
        VRTestAttribute attribute;
        XDSAddAttrElement addAttr;
        String attrName, attrFilterName;

        a = object.getModifiedAttributes().listIterator();
        while (a.hasNext())
        {
            attribute = (VRTestAttribute) a.next();
            attrName = attribute.getName();
            attrFilterName = null;

            if (this.useFilter)
            {
                attrFilterName = classFilter.getFilterName(attrName);
                if (attrFilterName == null)
                {
                    trace.trace("Filtered Attribute:  " + attrName);
                    continue;
                }
            }//if

            if (attrFilterName != null)
            {
                //if a schema mapping style sheet is used, matching
                //  may be case-sensitive, so it's best to use the
                //  schema-mapped name as supplied in the filter
                //  whenever possible
                attrName = attrFilterName;
            }

            addAttr = add.appendAddAttrElement();
            addAttr.setAttrName(attrName);
            addAttr.appendValueElement(attribute.getValue());
        }//while
    }//appendAddAttrElements(XDSAddElement, VRTestModifiedObject object,
     //                      VRTestClassFilter, String):void

}//class VRTestPublicationShim
