/*************************************************************************
Copyright © 1999-2002 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.skeleton;

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

/**
 * A basic skeleton for implementing a <code>SubscriptionShim</code>.
 *
 * The <code>SubscriptionShim</code> defines an interface for an application
 * driver to receive commands from DirXML. These commands must be executed on the
 * application on behalf of DirXML.
 *
 * @version 	2.0 28Jun2000
 */
public class SkeletonSubscriptionShim
	extends CommonImpl
	implements SubscriptionShim
{
	private static final String DRIVER_ID_VALUE 		= "JSKEL";
	private static final String DRIVER_VERSION_VALUE	= "1.1";
	private static final String DRIVER_MIN_ACTIVATION_VERSION	=	"0";
	/**
	 * Contains subscriber options from the init params passed to init()
	 */
	ShimParams params = null;

	/**
	 * Holds current connection state.
	 */
	private boolean connected = false;
	/**
	 * Return document that execute() sets up for use by command
	 * handlers. Reference is to &lt;output> element since all return information
	 * is placed under the &lt;output> element.
	 */
	private Element outputElement = null;
	/**
	 * This value is used to create an association in NDS between an NDS object and a
	 * hypothetical object in the skeleton driver's supported application.
	 *
	 * @see #addHandler
	 */
	private int currentAssociation = 1;
	/**
	 * Authentication info initialized by driver shim at init()
	 */
	private AuthenticationParams authParams = null;

	/**
	 * constructor, not strictly required, but a good idea
	 *
	 */
	SkeletonSubscriptionShim(AuthenticationParams authParams)
	{
		//set up the Trace object with the name of this object
		super("SkeletonSubscriptionShim");
		this.authParams = authParams;
	}

	/**
	 * <code>init</code> will be called before the first invocation of execute.
	 *
	 * @param initParameters XML document that contains the subscriber initialization
	 * parameters.
	 * @return XML document containing status/messages from init operation.
	 */
	public XmlDocument init(XmlDocument initParameters )
	{
		try
		{
			tracer.trace("init");
			//get any non-authentication options from the init document
			params = getShimParams(initParameters.getDocumentNS(),"subscriber",SUBSCRIBER_PARAMS);
			//get any state that may have been passed in (if this is our very first invocation, there
			//won't be any state)
			//The skeleton driver fakes associations to DirXML so that it appears more like a real driver
			//see addHandler()
			int assocState = params.getIntParam("current-association");
			if (assocState != -1)
			{
				//setup our fake association for handling adds
				currentAssociation = assocState;
			}
			//perform any other initialization that might be required.
			//Note that in the skeleton driver, SkeletonDriverShim.init() sets up a shared copy of
			//any authentication parameters for use by all three shim objects
			return createSuccessDocument();
		} catch (Throwable t)
		{
			//something bad happened...
			return	createStatusDocument(STATUS_FATAL,t.getMessage());
		}
	}

	/**
	 * <code>execute</code> will execute a command encoded in an XML document.
	 *
	 * @param doc	The document that contains the commands
	 * @param query	A query processor that can be used to query the the caller for more
	 * information
	 *
	 * @return This function will return a document that contains information
	 * of changes that should occur in NDS as a result of the application
	 * processing that changes that were passed in on this call. For example,
	 * the object was created in the application space and here is its
	 * unique identifier to add to the NDS object's association... It may also
	 * contain status and logging information.
	 */
	public XmlDocument execute( XmlDocument doc, XmlQueryProcessor query)
	{
		int	retryCount = 2;
		tracer.trace("execute");
		try
		{
			//setup the return document for use by command handlers
			outputElement = createOutputDocument();
			//try and connect with our mythical app
			while (retryCount-- > 0)
			{
				try
				{
					connect();
					Document document = doc.getDocumentNS();
					//find the <input> element
					Element input = (Element)document.getElementsByTagNameNS(null, "input").item(0);
					//iterate through the children, dispatching commands
					Node childNode = input.getFirstChild();
					while (childNode != null)
					{
						//only elements are interesting...ignore any interspersed text, comments, etc.
						if (childNode.getNodeType() == Node.ELEMENT_NODE)
						{
							dispatch((Element)childNode);
						}
						childNode = childNode.getNextSibling();
					}
					//return the result of whatever we were told to do
					return new XmlDocument(outputElement.getOwnerDocument());
				} catch (java.io.IOException e)
				{
					if (retryCount <= 0)
					{
						//done trying
						throw	e;
					}
				}
			}
			//if we fall through here, we failed to connect
			throw new java.io.IOException("failed to connect");
		} catch (java.io.IOException e)
		{
			//somehow failed in talking to app, tell DirXML to retry later
			return	createStatusDocument(STATUS_RETRY,e.toString());
		} catch (Throwable t)
		{
			//something bad happened...
			return	createStatusDocument(STATUS_ERROR,t.getMessage());
		}
	}

	/**
	 * Illustrative method. Connect to supported application if not already connected or if
	 * connection has been broken.
	 *
	 * @exception java.io.IOException If can't connect or connection broken.
	 */
	private void connect()
		throws java.io.IOException
	{
		if (!connected)
		{
			//do whatever is required to connect to the supported application -
			//e.g., open a Socket, etc.
			//if (connection fails)
			//{
			//	throw new java.io.IOException("Can't connect to application");
			//}
			connected = true;
		}
	}

	/**
	 * Dispatch a command to the appropriate handler.
	 *
	 * @param command The command element from the input document
	 */
	private void dispatch(Element command)
		throws Exception
	{
		String commandName = command.getNodeName();
		if (commandName.equals("add"))
		{
			addHandler(command);
		} else if (commandName.equals("modify"))
		{
			modifyHandler(command);
		} else if (commandName.equals("delete"))
		{
			deleteHandler(command);
		} else if (commandName.equals("rename"))
		{
			renameHandler(command);
		} else if (commandName.equals("move"))
		{
			moveHandler(command);
		} else if (commandName.equals("query"))
		{
			queryHandler(command);
		} else
		{
			//this won't happen unless a stylesheet rule adds some custom command
			//note that the status element is keyed with the event-id attribute from
			//the command
			addStatusElement(outputElement,
							STATUS_ERROR,
							"Unsupported command: '" + commandName + "'",
							command.getAttributeNS(null, "event-id"));
		}
	}

	/**
	 * Handle an &lt;add> command.
	 *
	 * @param the &lt;add> element.
	 */
	private void addHandler(Element add)
		throws	Exception
	{
		String className = add.getAttributeNS(null, "class-name");
		String eventId = add.getAttributeNS(null, "event-id");
		String srcDn = add.getAttributeNS(null, "src-dn");
		//don't build the string unless it's actually going out
		if (tracer.getTraceLevel() >= Trace.DEFAULT_TRACE)
		{
			tracer.trace("addHandler: class == '" + className + "'");
		}
		//do whatever is required here for the supported application
		//
		//In the case of this skeleton driver, we will setup a fake association value
		//for the <add> so that we will get modifies, deletes, etc. on the associated object.
		//
		//For a real driver, we would add an association using whatever unique key the application
		//can supply for the application object (a dn, a GUID, etc.)

		//get the output document for creating stuff
		Document document = outputElement.getOwnerDocument();
		//create the add-association element
		Element addAssociation = document.createElementNS(null, "add-association");
		//put it under the output element
		outputElement.appendChild(addAssociation);
		//add the attributes that tell DirXML to what NDS object this applies
		addAssociation.setAttributeNS(null, "dest-dn",srcDn);
		addAssociation.setAttributeNS(null, "event-id",eventId);
		//create the association value and put it under the add-association element
		Text value = document.createTextNode(Integer.toString(currentAssociation));
		addAssociation.appendChild(value);
		//increment our fake association value for the next add
		++currentAssociation;
		//and we're done
	}

	/**
	 * Handle a &lt;modify> command.
	 *
	 * @param the &lt;modify> element.
	 */
	private void modifyHandler(Element modify)
		throws	Exception
	{
		String className = modify.getAttributeNS(null, "class-name");
		String eventId = modify.getAttributeNS(null, "event-id");
		String assocValue = getAssociation(modify);
		//don't build the string unless it's actually going out
		if (tracer.getTraceLevel() >= Trace.DEFAULT_TRACE)
		{
			tracer.trace("modifyHandler: class == '" + className + "'");
		}
		//do whatever is required here for the supported application
		//use the assocValue to look up the object in the application, then
		//apply the attribute modifications

		//for the skeleton driver, just pretend everything worked
		addStatusElement(outputElement,STATUS_SUCCESS,null,eventId);
	}

	/**
	 * Handle a &lt;delete> command.
	 *
	 * @param the &lt;delete> element.
	 */
	private void deleteHandler(Element delete)
		throws	Exception
	{
		String className = delete.getAttributeNS(null, "class-name");
		String eventId = delete.getAttributeNS(null, "event-id");
		String assocValue = getAssociation(delete);
		//don't build the string unless it's actually going out
		if (tracer.getTraceLevel() >= Trace.DEFAULT_TRACE)
		{
			tracer.trace("deleteHandler: class == '" + className + "'");
		}
		//do whatever is required here for the supported application
		//use the assocValue to look up the object in the application, then perform the delete
		//for the skeleton driver, just pretend everything worked
		addStatusElement(outputElement,STATUS_SUCCESS,null,eventId);
	}

	/**
	 * Handle a &lt;rename> command.
	 *
	 * @param the &lt;rename> element.
	 */
	private void renameHandler(Element rename)
		throws	Exception
	{
		String className = rename.getAttributeNS(null, "class-name");
		String eventId = rename.getAttributeNS(null, "event-id");
		String assocValue = getAssociation(rename);
		//don't build the string unless it's actually going out
		if (tracer.getTraceLevel() >= Trace.DEFAULT_TRACE)
		{
			tracer.trace("renameHandler: class == '" + className + "'");
		}
		//do whatever is required here for the supported application
		//use the assocValue to look up the object in the application, then perform the rename
		//for the skeleton driver, just pretend everything worked
		addStatusElement(outputElement,STATUS_SUCCESS,null,eventId);
	}

	/**
	 * Handle a &lt;move> command.
	 *
	 * @param the &lt;move> element.
	 */
	private void moveHandler(Element move)
		throws	Exception
	{
		String className = move.getAttributeNS(null, "class-name");
		String eventId = move.getAttributeNS(null, "event-id");
		String assocValue = getAssociation(move);
		//don't build the string unless it's actually going out
		if (tracer.getTraceLevel() >= Trace.DEFAULT_TRACE)
		{
			tracer.trace("moveHandler: class == '" + className + "'");
		}
		//do whatever is required here for the supported application
		//use the assocValue to look up the object in the application, then perform the move
		//for the skeleton driver, just pretend everything worked
		addStatusElement(outputElement,STATUS_SUCCESS,null,eventId);
	}

	/**
	 * Handle a &lt;query> command.
	 *
	 * @param the &lt;query> element.
	 */
	private void queryHandler(Element query)
	{
		String eventId = query.getAttributeNS(null, "event-id");
		tracer.trace("queryHandler");
		//The query can specify a read of a single object with one or more attributes, or
		//it can specify a search for an object of a particular class, or various other things.
		//For a real driver, we would parse the query contents and make appropriate application calls
		//to execute the query. We would place the results of the query in zero or more <instance>
		//elements under the <output> element.
		//
		//see if this is a request for driver identification information
		Element searchClass = (Element)query.getElementsByTagNameNS(null, "search-class").item(0);
		if (searchClass != null)
		{
			String className = searchClass.getAttributeNS(null, "class-name");
			if (className != null && className.equals("__driver_identification_class__"))
			{
				addDriverIdentification(outputElement);
				return;
			}
		}
		//for the skeleton driver, just pretend everything worked but we didn't find anything (the
		//absence of an <instance> element indicates nothing was found)
		addStatusElement(outputElement,STATUS_SUCCESS,null,eventId);
	}

	/**
	 * Return the association value for a command element (add, modify, etc.)
	 *
	 * @param command The command element
	 * @return The association value, or null if no association found.
	 */
	private String getAssociation(Element command)
	{
		//find the association element as a child of the command
		Node childNode = command.getFirstChild();
		while (childNode != null)
		{
			if (childNode.getNodeType() == Node.ELEMENT_NODE &&
				childNode.getNodeName().equals("association"))
			{
				return	com.novell.xsl.util.Util.getXSLStringValue(childNode);
			}
			childNode = childNode.getNextSibling();
		}
		return	null;
	}

	/**
	 * Add subscriber state to the document to be returned at shutdown.
	 * This is called by <code>SkeletonDriverShim.shutdown()</code>.
	 *
	 * @param output The output element for the document that will be returned to DirXML.
	 */
	void setState(Element output)
	{
		//add our state to be saved at shutdown
		addState(output,"subscriber-state","current-association",Integer.toString(currentAssociation));
	}
	
	/**
	 * Add driver identification information to the output document in response to 
	 * an engine query.
	 *
	 * @param output output element of output document.
	 */
	void addDriverIdentification(Element output)
	{
		Document doc = output.getOwnerDocument();
		Element	instance = doc.createElementNS(null, "instance");
		output.appendChild(instance);
		instance.setAttributeNS(null, "class-name","__driver_identification_class__");
		Element	attr = doc.createElementNS(null, "attr");
		instance.appendChild(attr);
		attr.setAttributeNS(null, "attr-name","driver-id");
		Element value = doc.createElementNS(null, "value");
		attr.appendChild(value);
		value.setAttributeNS(null, "type","string");
		Text text = doc.createTextNode(DRIVER_ID_VALUE);
		value.appendChild(text);
		attr = doc.createElementNS(null, "attr");
		instance.appendChild(attr);
		attr.setAttributeNS(null, "attr-name","driver-version");
		value = doc.createElementNS(null, "value");
		attr.appendChild(value);
		value.setAttributeNS(null, "type","string");
		text = doc.createTextNode(DRIVER_VERSION_VALUE);
		value.appendChild(text);
		attr = doc.createElementNS(null, "attr");
		instance.appendChild(attr);
		attr.setAttributeNS(null, "attr-name","min-activation-version");
		value = doc.createElementNS(null, "value");
		attr.appendChild(value);
		value.setAttributeNS(null, "type","int");
		text = doc.createTextNode(DRIVER_MIN_ACTIVATION_VERSION);
		value.appendChild(text);
	}

	/**
	 * Table of parameters that this subscriber shim wants to get from the &lt;subscriber-options> element
	 * of the init-params.
	 * <p>
	 * This is illustrative, of course, an actual driver would want parameters specific to its
	 * functionality.
	 */
	private static final ShimParamDesc[] SUBSCRIBER_PARAMS =
	{
		//options
		new ShimParamDesc("sub-1",ShimParamDesc.STRING_TYPE,false),
		//state
		new ShimParamDesc("current-association",ShimParamDesc.INT_TYPE,false)
	};
}
