/*
 * Created on Sep 29, 2003
 *
 * To change the template for this generated file go to
 * Window>Preferences>Java>Code Generation>Code and Comments
 */
package com.ezgov.saf.applayer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.namespace.QName;

import org.apache.commons.jxpath.AbstractFactory;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.Pointer;

import com.ezgov.saf.applayer.exceptions.*;

/**
 * @author fvermeul
 *
 * To change the template for this generated type comment go to
 * Window>Preferences>Java>Code Generation>Code and Comments
 */
public class ModelManagerImpl implements ModelManager {

	/*
	 * Custom factory class to deal with the JAXB List type for
	 * complex sequence types
	 * @author fvermeul
	 *
	 * To change the template for this generated type comment go to
	 * Window>Preferences>Java>Code Generation>Code and Comments
	 */
	class ListElementFactory extends AbstractFactory {

		/**
		 * This JXPath custom factory class initilizes a List element
		 * before it is attempted to set a value into it
		 */
		public ListElementFactory() {
			super();
		}

		public boolean createObject(JXPathContext context, Pointer pointer, 
									Object parent, String name, int index) 
			{
				//	If the target object is a List, initialize it with sufficient
				//	entries for the subsequent set(index) to succeed
				String	thePath	=	new	String("/" + name);
				Object	theObject	=	context.getValue(thePath);
				if	(theObject	instanceof	List)	{
					List	theList	=	(List)theObject;
					int number	=	index + 1 - theList.size();
					for	(int i = 0; i < number; i++)
						theList.add(null);
				}
				return true;
			}

	}



	/**
	 * 
	 */
	public ModelManagerImpl() {
		super();
		// TODO Auto-generated constructor stub
	}
	
	/* (non-Javadoc)
		 * @see com.ezgov.framework.applayer.ModelManager#setModel(java.lang.String)
		 */
	public void setModel(String packageName) throws ModelException {
		// TODO Auto-generated method stub

		_jaxbFactory	=	createJAXBFactory(packageName);		
	}	
	
	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#addObject(java.lang.Object, java.lang.String)
	 */
	public void addObject(Object object, String targetXPath) 
					throws ModelException {
			
		String	name	=	getObjectName(new	StringBuffer(targetXPath));
		if	(name	!=	null)
			_objects.put(name, object);
		else
			throw	new	ModelException("Object name must start with $");
	}
		
	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#createObject(javax.xml.namespace.QName, java.lang.String, java.util.Map)
	 */
	public void createObject(QName type, String targetXPath, Map attributes)
		throws ModelException {
			
		// Use JAXB object factory to create the object
		Object	newObject	=	create(type);
		
		// Set the attributes
		Set			keys	=	attributes.keySet();
		Iterator	iterator	=	keys.iterator();
		while	(iterator.hasNext())	{
			String	path	=	(String)iterator.next();
			setObjectProperty(newObject, path, attributes.get(path));
		}
		
		// Save object under targetXPath
		_objects.put(targetXPath, newObject);

	}

	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#updateObject(java.lang.String, com.ezgov.framework.applayer.ApplicationObject)
	 */
	public void updateObject(String targetXPath, Object object)
		throws ModelException {
		
		StringBuffer	fullPath	=	new	StringBuffer(targetXPath);
		Object	target	=	resolveSubstitution(fullPath);
		if	(target	==	null)
			throw	new	ModelException("Path does not identify a known target object");
			
		setObjectProperty(target, fullPath.toString(), object);
	}


	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#readObject(java.lang.String)
	 */
	public Object readObject(String targetXPath)
		throws ModelException {
			
		StringBuffer	fullPath	=	new	StringBuffer(targetXPath);
		Object	target	=	resolveSubstitution(fullPath);
		if	(target	==	null)
			throw	new	ModelException("Path does not identify a known target object");
		if	(fullPath.toString().length()	==	0)
			return	target;
		
		Object	readObj	=	getObjectProperty(target, fullPath.toString());
		
		return readObj;
	}

	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#readObjects(java.lang.String)
	 */
	public Map readObjects(String targetXPath) throws ModelException {
		
		StringBuffer	fullPath	=	new	StringBuffer(targetXPath);
		Object	target	=	resolveSubstitution(fullPath);
		if	(target	==	null)
			throw	new	ModelException("Path does not identify a known target object");
		Map	objects	=	getObjectProperties(target, fullPath.toString());
		
		return objects;
	}

	/* (non-Javadoc)
	 * @see com.ezgov.framework.applayer.ModelManager#deleteObject(java.lang.String)
	 */
	public void deleteObject(String targetXPath) throws ModelException {
		
		StringBuffer	fullPath	=	new	StringBuffer(targetXPath);
		Object	target	=	resolveSubstitution(fullPath);
		if	(target	==	null)
			throw	new	ModelException("Path does not identify a known target object");
		
		if	(isSubscript(fullPath.toString()))	{
			removeFromList(target, fullPath);		
		}
		else
			setObjectProperty(target, fullPath.toString(), null);
	}
	


	protected	Object	createJAXBFactory(String	packageName)	
							throws ModelException	{
								
		Object	factory	=	null;								
		try {
			Class	theClass = Class.forName(packageName + "." + FACTORY_NAME);
			factory	=	theClass.newInstance();
		} catch (ClassNotFoundException e) {
			throw	new	ModelException("Model factory does not exist");
		} catch (InstantiationException e) {
			throw	new	ModelException("Cannot instantiate model factory class");
		} catch (IllegalAccessException e) {
			throw	new	ModelException("Cannot access model factory class");
		}
						
		return	factory;
	}
	
	protected	Object	create(QName	type)	
										throws ModelException	{
	
		//	Use reflection to invoke the appropriate method on the JAXB factory
		//	object
		Object	theObject	=	null;
		String	methodName	=	new	String("create" + type.getLocalPart());
		Class[] argTypes	=	new	Class[0];
		try {
			Method	method		=	_jaxbFactory.getClass().getMethod(methodName, 
																	  argTypes);
			Object[]	args	=	new	Object[0];
			theObject	=	method.invoke(_jaxbFactory, args);
		} catch (SecurityException e) {
			throw	new	ModelException("Security exception while creating object", e);
		} catch (NoSuchMethodException e) {
			throw	new	ModelException("JAXB factory method for this type of object does not exist", e);
		} catch (IllegalArgumentException e) {
			throw	new	ModelException("JAXB factory create method argument type mismatch", e);
		} catch (IllegalAccessException e) {
			throw	new	ModelException("Cannot access JAXB factory method", e);
		} catch (InvocationTargetException e) {
			throw	new	ModelException("JAXB factory invocation error", e);
		}

		return	theObject;

	}
	
	protected	void	setObjectProperty(Object	target, String	path, 
										  Object	value) 
						throws ModelException	{
										  	
		JXPathContext context = JXPathContext.newContext(target);
		try	{
			if	(isSubscript(path))	{
				//	If the target property is of the List type, we need to
				//	initialize it with an extra entry before calling the
				//	JAXP "setValue()" method
				context.setFactory(new ListElementFactory());
				context.createPath(path);
			}
			context.setValue(path, value);
		}
		catch	(JXPathException	e)	{
			throw	new	ModelException("Invalid property path or value", e);
		}
												  	
	}
	
	protected	Object	getObjectProperty(Object	target, String path) 
						throws ModelException	{
		
		Object	value	=	null;
		
		JXPathContext context = JXPathContext.newContext(target);
		try	{
			value	=	context.getValue(path);
		}
		catch	(JXPathException	e)	{
			throw	new	ModelException("Invalid property path", e);
		}
				
		return	value;
	}
	
	protected	Map	getObjectProperties(Object	target, String path) 
					throws ModelException	{

		Iterator	iterator	=	null;
		HashMap		objects		=	new	HashMap();
		
		JXPathContext context = JXPathContext.newContext(target);
		try	{
			iterator	=	context.iteratePointers(path);
			while	(iterator.hasNext())	{
				Pointer	pointer	=	(Pointer)iterator.next();
				String	objPath	=	pointer.asPath();
				Object	objValue	=	pointer.getValue();
				objects.put(objPath, objValue);
			}
			
		}
		catch	(JXPathException	e)	{
			throw	new	ModelException("Invalid property path", e);
		}
				
		return	objects;
	}
	
	protected	Object	resolveSubstitution(StringBuffer	path) 
						throws ModelException	{
		
		Object	target	=	null;
		String	name	=	getObjectName(path);
		if	(name	==	null)
			throw	new	ModelException("Path does not start with a $name");
			
		target		=	_objects.get(name);
		
	
		return	target;
	}
	
	protected	String	getObjectName(StringBuffer	path)	{

		String	name	=	null;
		int	index	=	path.indexOf(VAR_DELIMITER);
		
		if	(index	==	0)	{
			//	OK, $xyz can only be used as first location in the XPath
			int	endIndex	=	path.indexOf(LOCATION_DELIMITER);
			if	(endIndex == -1)	{
				name	=	path.substring(1);
				endIndex	=	path.length();
				path.replace(0, endIndex, "");
			}
			else	{
				name	=	path.substring(1, endIndex);
				String	relPath	=	path.substring(endIndex+1);
				endIndex	=	path.length();
				path.replace(0, endIndex, relPath);
			}
				
		}

		return	name;			
	}
	
	/**
	 * @param target
	 * @param fullPath
	 */
	protected void removeFromList(Object target, StringBuffer fullPath) throws ModelException {
		
		//	Strip [i] construct from path
		int	index	=	fullPath.lastIndexOf("[");
		String	indexString	=	fullPath.substring(index+1,index+2);
		Integer	indexValue	=	new	Integer(indexString);
		int	listIndex	=	indexValue.intValue() - 1;
		if	(index	>	0)	{
			String	propertyPath	=	fullPath.substring(0, index);
			Object	property	=	getObjectProperty(target, propertyPath);
			if	(property	instanceof	List)	{
				List	list	=	(List)property;
				list.remove(listIndex);
			}
		}
	}	
	
	private	boolean	isSubscript(String	path)	{
		
		StringTokenizer	tokenizer	=	new	StringTokenizer(path, 
															LOCATION_DELIMITER);
		String	token	=	null;
		int	nrOfTokens	=	tokenizer.countTokens();
		boolean	isSubscript	=	false;
		
		for	(int i = 1; i < nrOfTokens; i++)
			token	=	tokenizer.nextToken();
		// now get the last token
		token	=	tokenizer.nextToken();
		int	begin	=	token.indexOf("[");
		if	(begin	> 0)	{
			int	end	=	token.indexOf("]", begin);
			String	index	=	token.substring(begin+1, end);
			try	{
				Integer	indexInt	=	Integer.decode(index);
				isSubscript	=	true;
			}
			catch	(NumberFormatException	e)	{
			}
		}
		
		return	isSubscript;
	}
	
	private	Object	_jaxbFactory	=	null;
	private	HashMap	_objects		=	new	HashMap();
	
	private	static	final	String	FACTORY_NAME = "ObjectFactory";
	private	static	final	String	VAR_DELIMITER = "$";
	private	static	final	String	LOCATION_DELIMITER = "/";
	
		

}

