/*
 * MiningMart Version 1.0
 * 
 * Copyright (C) 2006 Martin Scholz, Timm Euler, 
 *                    Daniel Hakenjos, Katharina Morik
 *
 * Contact: miningmart@ls8.cs.uni-dortmund.de
 *
 * A list of contributing developers (other than the copyright 
 * holders) can be found at
 * http://mmart.cs.uni-dortmund.de/downloads/download.html
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program, see the file MM_HOME/LICENSE; if not, write
 * to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 */
package edu.udo.cs.miningmart.m4.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Writer;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Vector;

import edu.udo.cs.miningmart.db.DB;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.exception.XmlException;
import edu.udo.cs.miningmart.m4.core.Assertion;
import edu.udo.cs.miningmart.m4.core.Case;
import edu.udo.cs.miningmart.m4.core.Column;
import edu.udo.cs.miningmart.m4.core.ColumnStatistics1;
import edu.udo.cs.miningmart.m4.core.ColumnStatistics2;
import edu.udo.cs.miningmart.m4.core.Columnset;
import edu.udo.cs.miningmart.m4.core.ColumnsetStatistics;
import edu.udo.cs.miningmart.m4.core.Condition;
import edu.udo.cs.miningmart.m4.core.Constraint;
import edu.udo.cs.miningmart.m4.core.Coordinates;
import edu.udo.cs.miningmart.m4.core.Key;
import edu.udo.cs.miningmart.m4.core.KeyMember;
import edu.udo.cs.miningmart.m4.core.M4Data;
import edu.udo.cs.miningmart.m4.core.M4Object;
import edu.udo.cs.miningmart.m4.core.OpParam;
import edu.udo.cs.miningmart.m4.core.Operator;

/**
 * 
 * @author Martin Scholz
 * @version $Id: M4Xml.java,v 1.5 2006/04/11 14:10:10 euler Exp $
 */
public class M4Xml {
	
	// ********************************************
	// *                  Export	              *
	// ********************************************

	private static final boolean EXPORT_RELATIONAL = false;
	
	private static final HashMap xmlIds = new HashMap();

	private static long nextXmlId;
	
	public static Long getExistingXmlId(XmlInfo object) {
		return (Long) xmlIds.get(object);
	}

	public static long setNewXmlId(XmlInfo object) {
		long theId = nextXmlId++;
		xmlIds.put(object, new Long(theId));
		return theId;
	}
	
	public static synchronized void exportCase(edu.udo.cs.miningmart.m4.Case m4Case, Writer writer)
		throws IOException
	{
		nextXmlId = 1;
			
		final String dataTag = "Data";
		writer.write(createOpeningTag(dataTag) + "\n");
		writeVersion(writer);
		
		HashSet open = new HashSet();
		open.add(m4Case);
		while (! open.isEmpty()) {
			Iterator it = (new Vector(open)).iterator();
			while (it.hasNext()) {
				Object obj = it.next();
				if (obj instanceof XmlInfo) {		
					M4Xml.export((XmlInfo) obj, writer, open);
					open.remove(obj);
				}
				else {
					throw new IOException( // another type of exception might do better ...
						"Internal Error: Class " + obj.getClass().getName()
						+ " found during M4Xml.exportCase(Case, writer),\n"
						+ " which does not implement the interface XMlInfo!\n");	
				}
			}
		}

		writer.write(createClosingTag(dataTag) + "\n");
		writer.flush();
		writer.close();
		xmlIds.clear();
	}

	/**
	 * This method serializes a given object and writes the <code>String</code> using
	 * a given <code>Writer</code> object. All objects referenced by this object will
	 * also be serialized (before) and be referenced by their XML ID.
	 * 
	 * To be able to reach all objects of a specified <code>Case</code> it is necessary
	 * to collect all objects referencing <code>this</code> object. Thus the method
	 * will add all objects not yet serialized and having a reference <b>to</b> (!)
	 * <code>this</code> object to the given <code>Collection</code>.
	 * 
	 * @param out the <code>Writer</code> to write the serialization
	 * <code>String</code> to
	 * @param dependent a <code>Collection</code> to be filled with objects referencing
	 * <code>this</code> object
	 * @return the XML ID of <code>this</code> object. Please note that this ID is
	 * different from the object's M4 ID! A return value of <code>0</code> indicates
	 * that the object was not exported but skipped, because it refers to the relational
	 * level which is not to be exported.
	 */
	public static long export(XmlInfo m4d, Writer out, Collection dependent) throws IOException
	{	
		if (m4d == null || out == null || dependent == null) {
			throw new IOException("<Null> parameter found in M4Xml.export(M4Data, Writer, Collection) !");
		}

		// Just skip relational objects if they are not to be exported:
		if ( ! EXPORT_RELATIONAL && M4Xml.isRelationalObject(m4d) ) {
			return 0;						
		}

		Long idL;
		if ( (idL = M4Xml.getExistingXmlId(m4d)) != null) {
			return idL.longValue(); // This object has already been exported!
		}

		/* Get an Id and prohibit another second export of this object: */
		final long xmlId = M4Xml.setNewXmlId(m4d);
		
		/* Export all of the object this object needs to reference: */
		Collection attribDescriptions = M4Xml.exportContainedObjects(m4d, out, dependent);
		
		/* In case of static system objects the only description is the M4 ID: */
		if (M4Xml.isM4SystemObject(m4d)) {
			 // isM4SystemObject implies that it is assignment-compatible to M4Data
			long m4Id = ((M4Data) m4d).getId();
			attribDescriptions.clear();
			String m4IdXml = M4Xml.putInXmlTags(Long.toString(m4Id), XmlInfo.TAG_M4_ID);
			attribDescriptions.add(m4IdXml);
		}

		/* Writing the object to flat file: */
		String myTag = m4d.getObjectTag();
		
		String lev1Space = "  ";
		String lev2Space = "    ";
		out.write(lev1Space + M4Xml.createOpeningTag(myTag) + "\n");
		out.write(lev2Space + M4Xml.putInXmlTags(Long.toString(xmlId), XmlInfo.TAG_XML_ID) + "\n");
		Iterator it = attribDescriptions.iterator();
		while (it.hasNext()) {
			String attribXml = (String) it.next();
			if (attribXml != null) {
				out.write(lev2Space + attribXml + "\n");
			}
		}
		out.write(lev1Space + M4Xml.createClosingTag(myTag) + "\n");

		/* 
		 * Finally follow the foreign key references in the other direction 
		 * and add all those objects to the "open list" if not yet exported:
		 */
		try {
			it = m4d.getDependentObjects().iterator();
			while (it.hasNext()) {
				Object obj = it.next();
				if (obj instanceof XmlInfo) {
					XmlInfo xmli = (XmlInfo) obj;
					if ( M4Xml.getExistingXmlId(xmli) == null
					   && ! M4Xml.isM4SystemObject(xmli) )
					{						
						dependent.add(xmli);
					}
				}
			}
		}
		catch (M4Exception e) {
			m4d.doPrint(Print.MAX,
				"Error when trying to receive the dependent objects of an "
				+ m4d.getClass().getName() + "-object");
			if (m4d instanceof M4Object) {
				m4d.doPrint(Print.MAX, "(ID: " + ((M4Object) m4d).getId() + ")");
			}
			m4d.doPrint(Print.MAX, "while trying to export that object.\nMessage was:\n");
			m4d.doPrint(e);
		}
		return xmlId;
	}

	/**
	 * Indicates whether to skip a specified M4 relational Object when exporting to XML.
	 * This method will never return <code>true</code> for non-<code>M4Data</code>
	 * objects!
	 */
	private static boolean isRelationalObject(XmlInfo m4d) {
		for (int i=0; i<M4_DATA_RELATIONAL_CLASSES.length; i++) {
			if (M4_DATA_RELATIONAL_CLASSES[i].isInstance(m4d)) {
				return true;
			}
		}
		return false;
	}

	// Skip these classes at regular imports although they are sub-classes of M4Data,
	// because they contain relational information.
	private static Class[] M4_DATA_RELATIONAL_CLASSES = {
		Column.class, Columnset.class, ColumnsetStatistics.class,
		ColumnStatistics1.class, ColumnStatistics2.class,
		Key.class, KeyMember.class
	};		

	/**
	 * Indicates whether to skip a specified M4 system Object when exporting to XML.
	 * This method will never return <code>true</code> for non-<code>M4Data</code>
	 * objects!
	 */
	private static boolean isM4SystemObject(XmlInfo m4d) {
		for (int i=0; i<M4_DATA_SYSTEM_CLASSES.length; i++) {
			if (M4_DATA_SYSTEM_CLASSES[i].isInstance(m4d)) {
				return true;
			}
		}
		return false;
	}

	// Skip these classes at regular imports although they are sub-classes of M4Data,
	// because they store static system information not to be written back to the DB.
	private static Class[] M4_DATA_SYSTEM_CLASSES = {
		Operator.class,	Assertion.class, Condition.class, Constraint.class,	OpParam.class
	};		

	/**
	 * @param out the <code>Writer</code> object to write the serialization with
	 * @param dependent a <code>Collection</code> of objects referencing the objects
	 * already serialized
	 * 
	 * @return a <code>Collection</code> of <code>String</code>s, each of which
	 * is a reference to a primitive datatype, or an XML Id reference to an
	 * <code>M4Data</code> Java object <code>this</code> object holds a foreign
	 * key reference to.
	 */
	public static Collection exportContainedObjects(XmlInfo m4d, Writer out, Collection dependent)
		throws IOException
	{
		M4Info m4i = m4d.getXmlInfo();
		Iterator it = m4i.getInfos().iterator();
		Vector ret = new Vector();
		String error =  "\nError while exporting dependent objects of an object of class "
						+ m4d.getClass().getName() + "!\nException message was:\n";
		
		while (it.hasNext()) {
			M4InfoEntry entry = (M4InfoEntry) it.next();
			try {
				String xmlDescription = M4Xml.createXmlDescription(m4d, entry, out, dependent);
				ret.add(xmlDescription);
			}
			catch (M4Exception e) {
				m4d.doPrint(Print.MAX, error + e.getMessage() + "\n");
			}
		}
		
		// Enable non-generic parts in the XML description of this object:
		try {
			Collection col = m4d.exportLocal(out, dependent);
			if (col != null) {
				ret.addAll(col);
			}
		} 
		catch (M4Exception e) {
			m4d.doPrint(Print.MAX, error + e.getMessage() + "\n");
		}
		
		return ret;	
	}

	/**
	 * Method creating an XML description of an attribute. If the attributes holds
	 * a foreign key reference to a object that was not yet serialized, then the
	 * serialization is done before returning a description based on the referenced
	 * objects new XML ID.
	 * 
	 * @param entry an <code>M4InfoEntry</code> describing one of <code>this</code>
	 * objects attributes and the corresponding getter and datatype.
	 * @param out a <code>Writer</code> for writing serialization of referenced objects
	 * to if not yet done.
	 * @param dependent a <code>Collection</code> of objects referencing the objects
	 * already serialized. Just to be used when calling <code>export</code> for
	 * another object referenced by <code>this</code> object.
	 * 
	 * @return an XML string describing corresponding value for the specified entry
	 * or <code>null</code>, if the value of the entry was <code>null</code>.
	 */	
	public static String createXmlDescription(XmlInfo m4o, M4InfoEntry entry, Writer out, Collection dependent)
		throws M4Exception, IOException
	{
		Object object = m4o.genericGetter(entry.getGetter());
		if (object == null) {
			return null; // currently we just skip NULL values	
		}
		
		String value;
		Class objClass = entry.getTheObjectClass();
		if (XmlInfo.class.isAssignableFrom(objClass) || object instanceof XmlInfo) {
			long objId = M4Xml.export((XmlInfo) object, out, dependent);
			if (objId == 0) {
				return null; // (objId == 0) indicates that we skip a relation object!
			}
			value = putInXmlTags(Long.toString(objId), XmlInfo.TAG_XML_ID);
		}
		else if (objClass.equals(String.class)) {
			value = putInXmlTags(M4Xml.escapeXmlString((String) object), XmlInfo.TAG_STRING);
		}
		else if (objClass.equals(long.class) || objClass.equals(Long.class)) {
			value = putInXmlTags(((Long) object).toString(), XmlInfo.TAG_LONG);
		}
		else if (objClass.equals(int.class) || objClass.equals(Integer.class)) {
			value = putInXmlTags(((Integer) object).toString(), XmlInfo.TAG_INTEGER);
		}
		else if (objClass.equals(short.class) || objClass.equals(Short.class)) {
			value = putInXmlTags(((Short) object).toString(), XmlInfo.TAG_SHORT);
		}
		else if (objClass.equals(double.class) || objClass.equals(Double.class)) {
			value = putInXmlTags(((Double) object).toString(), XmlInfo.TAG_DOUBLE);
		}
		else if (objClass.equals(Collection.class)) {
			value = M4Xml.serializeM4DataCollection((Collection) object, out, dependent);
			if (value == null) {
				return null;
			}
		}
		else {
			throw new M4Exception(
			"Unsupported object type in M4InfoEntry for attribute "
			+ entry.getDbAttribute() + " found: " + objClass.getName()
			+ "\nClass to be exported is " + m4o.getClass().getName());
		}

		return putInXmlTags(value, entry.getDbAttribute());
	}

	/**
	 * Service method to serialize an arbitrary <code>Collection</code>
	 * of <code>M4Data</code> objects to XML.
	 * 
	 * @param col the <code>Collection</code> of <code>XmlInfo</code> objects
	 * @param out the <code>Writer</code> of the serialization
	 * @param dependent the <code>Collection</code> of dependent objects
	 * 
	 * @return a <code>String</code> describing the collection or <code>null</code>
	 * if the <code>Collection</code> was <code>null</code> or empty
	 */
	public static String serializeM4DataCollection(Collection col, Writer out, Collection dependent)
		throws M4Exception, IOException
	{
		if (col == null || col.isEmpty()) {
			return null;	
		}
		Iterator it = col.iterator();
		StringBuffer buf = new StringBuffer();
		while (it.hasNext()) {
			Object m4d = it.next();
			if (m4d instanceof XmlInfo) {
				long xmlId = M4Xml.export((XmlInfo) m4d, out,dependent);
				if (xmlId != 0) { // otherwise we skip a relational object not to be exported
					String xmlTag = putInXmlTags(Long.toString(xmlId), XmlInfo.TAG_XML_ID);
					buf.append(xmlTag);
				}
			}
			else {
				throw new M4Exception(
					"Found class not implementing XmlInfo in Collection for static method"
					+ " M4Xml.serializeM4DataCollection(Collection, Writer, Collection)!\n"
					+ "Class was: " + m4d.getClass().getName() + "\n");	
			}
		}
		return putInXmlTags(buf.toString(), XmlInfo.TAG_COLLECTION);
	}
	
	/**
	 * Simple service method.
	 * @param text a text to put in XML tags.
	 * @param xmlTag the tag text without &gt; and &lt;
	 * @return the tagged text <code>String</code>
	 */
	public static String putInXmlTags(String text, String xmlTag) {
		if (text == null) {
			text = "";
		}
		return createOpeningTag(xmlTag) + text + createClosingTag(xmlTag);
	}
	
	public static String createOpeningTag(String tagName) {
		if (tagName == null)
			tagName = "";
		return "<" + tagName.trim() + ">";
	}
	
	public static String createClosingTag(String tagName) {
		if (tagName == null)
			tagName = "";
		return "</" + tagName.trim() + ">";
	}
	
	// ********************************************
	// *                  Import	              *
	// ********************************************

	// Stores the XML IDs of all the imported XML objects.
	// These are different from the M4 IDs !!
	private static final HashMap xmlObjects = new HashMap();
	
	// A cache for the mapping from XML tags for M4 objects to
	// the correspronding classes. This mapping also rules out
	// classes which are not to be importet, either beacuse they
	// contain relational level information the user explicitly
	// does not want to be imported, or because they belong to
	// M4 system classes.
	private static final HashMap xmlTagToClass = new HashMap();

	private static XmlInfo getXmlObject(Long xmlId) {
		return (XmlInfo) xmlObjects.get(xmlId);
	}

	private static void storeXmlObject(Long xmlId, XmlInfo object) {
		xmlObjects.put(xmlId, object);
	}
	
	/**
	 * Import a Case from the given input stream. Set the DB object for the
	 * case to the given one. Check that the file in the input stream was created
	 * with the given xmlVersion. Version Strings are stored in the interface XmlInfo.java.
	 */
	public static synchronized Case importCase(InputStream in, DB db, String xmlVersion)
		throws IOException, XmlException, M4Exception
	{
		db.clearM4Cache();

		nextXmlId = 100000;
		LineNumberReader reader = new LineNumberReader(new InputStreamReader(in));
		Case m4case = null;

		String line = skipDataAndVersion(reader, xmlVersion);

		String tag = null;
		do {
			String[] tlt = findTopLevelTag(line, reader);
			if (tlt != null) {
				tag = tlt[0];
				String embedded = tlt[1];
				line = tlt[2];
				
				// debug:
				if (tag.equals("Projection")) {
					int i = 0;
					int j = i;
				}

				if (tag.equals("")) { // </data> found
					tag = null;
					// XML export file was correctly closed!
				}
				else {
					tag = tag.trim();
					Class classObj = (Class) xmlTagToClass.get(tag);
					if (classObj == null) {
						String className;
						try {
							className = "edu.udo.cs.miningmart.m4.core." + tag;
							classObj = Class.forName(className);
						}
						catch (ClassNotFoundException e) {
							throw new XmlException(
								"Unknown tag! Could not find a class matching the XML tag "
								+ tag + "!");
						}
						
						xmlTagToClass.put(tag, classObj);
						if ( ! XmlInfo.class.isAssignableFrom(classObj)) {
								throw new XmlException(
									"Found an incompatible Class in method importCase(InputStream, DB)!"
									+ "\nClass found according to tag in InputStream is " + classObj.getName()
									+ ", but this class does not implement the XmlInfo interface!\n");	
						}
					}

					XmlInfo object = (XmlInfo) db.createNewInstance(classObj);
					object.doPrint(Print.DB_READ,
						"\nImporting " + classObj.getName() + ":\n" + embedded + "\n");
					
					Long xmlId = M4Xml.xmlImport(object, embedded);
					if (xmlId == null) {
						throw new XmlException(
							"The following object description for a " + tag
							+ " does not contain the mandatory tag "
							+ XmlInfo.TAG_XML_ID + "!");
					}
				
					if (object instanceof Case) {
						if (m4case != null) {
							throw new XmlException(
								"M4Xml.importCase(InputStream, DB, String):\n"
								+ "Found multiple cases in the specified export file!\n"
								+ "Name of the first is: " + m4case.getName()
								+ "\nName of the second is: " + ((Case) object).getName() + "\n");
						}
						m4case = (Case) object;	
					}
				}
			}
		} while (tag != null);

		reader.close();
		xmlIds.clear();
		
		if (m4case == null) {
			throw new XmlException(
				"M4Xml.importCase(InputStream, DB, String):\n"
				+ "No case found in the specified export file!\n");
		}
		
		return m4case;
	}

	/**
	 * Helper method to skip the leading data tag and version information.
	 * @return the rest of the line after the version information, never <code>null</code>
	 */
	private static String skipDataAndVersion(LineNumberReader reader, String xmlVersion)
		throws IOException, XmlException
	{
		String line = "";
		int index = -1;

		while ( line != null && index == -1) { // ( (no EOF) && (no <data> found) )
			line = reader.readLine();
			index = (line == null ? -1 : line.toLowerCase().indexOf("<data>"));
		}
			
		if (line == null) {
			throw new XmlException("Found <EOF> but no XML data in import file!");
		}
			
		line = line.substring(index + "<data>".length());
		if (line.indexOf("</data>") != -1) {
			throw new XmlException("Found closing data tag before any data in XML import file!");
		}		

		// checkVersion sets the line to the first line after the version information
		return checkVersion(reader, xmlVersion, line);		
	}
	
	/* The tag to be used to tag version information in the export file. */
	private static final String TAG_VERSION = "Version";
	
	/*
	 * Set the version that is specified in the XmlInfo-Interface into
	 * the export file.
	 */
	private static void writeVersion(Writer writer) throws IOException
	{
		String versionTag = putInXmlTags(XmlInfo.M4_XML_VERSION, TAG_VERSION);
		String versionText = "  " + versionTag + "\n";
		writer.write(versionText);
	}
	
	/*
	 * Check that the file read by the given reader was created with the given MM-XML-version.
	 * Returns the first line in the file after the version information.
	 */
	private static String checkVersion(LineNumberReader reader, String version, String line) 
	        throws IOException, XmlException
	{
		String[] tagInfo = findTopLevelTag(line, reader);
		
		// check if tag is correct:
		if (tagInfo == null)
		{   throw new XmlException("Could not find version information in XML input file!");  }
		
		if ( ! tagInfo[0].equals(TAG_VERSION))
		{   throw new XmlException("Corrupted version information in XML input file! Expected tag '" + 
			                       TAG_VERSION + "', but found '" + tagInfo[0] + "'!");
		}
			
		// check if version info is correct:
		String versionInfoFromFile = tagInfo[1].trim();
		if ( ! versionInfoFromFile.equals(version))
		{   
			throw new XmlException("Import file was created with XML version " +
			                       versionInfoFromFile +
			                       ", but expected was " +
			                       version + "!");
		}
		
		// return next line after version info:
		return reader.readLine();
	}

	/**
	 * @return <code>null</code> if an EOF was found unexpectedly,
	 * or <code>String[3]</code> with<ul>
	 * <li><code>String[0]</code>: name of the tag</li>
	 * <li><code>String[1]</code>: <code>String</code> enclosed by tag</li>
	 * <li><code>String[2]</code>: rest of last line
	 * </ul>, or <code>String[0].equals("")</code> if the closing tag of
	 * the XML file was found in a syntactically correct place.
	 */
	private static String[] findTopLevelTag(String line, LineNumberReader reader)
		throws IOException
	{
		{
			int startIndex = (line == null ? -1 : line.indexOf("<"));
			while (line != null && startIndex == -1) {
				line = reader.readLine();
				startIndex = (line == null ? -1 : line.indexOf("<"));
			}
			if (line == null)
				return null;
	
			line = line.substring(startIndex);
		}

		String[] ret = new String[3];
		final String tag;
		final String closeTag;
		{
			int tagEnd = line.indexOf(">");
			if (tagEnd == -1)
				return null;
		
			tag = line.substring(1, tagEnd);
			if (tag.equalsIgnoreCase("/data")) {
				ret[0] = "";
				return ret;	
			}

			line = line.substring(tagEnd + 1);
			closeTag = "</" + tag + ">";
		}
		
		StringBuffer buf = new StringBuffer();
		{
			int endIndex = (line == null ? -1 : line.indexOf(closeTag));
			while (line != null && endIndex == -1) {
				buf.append(line + "\n");
				line = reader.readLine();
				endIndex = (line == null ? -1 : line.indexOf(closeTag));
			}
			if (line == null) {
				return null;	
			}
			
			buf.append(line.substring(0, endIndex));
			line = line.substring(endIndex + closeTag.length());
		}
		
		ret[0] = tag;
		ret[1] = buf.toString();
		ret[2] = line;
		return ret;
	}

	/** Sets the fields of this object according to the provided XML description */
	public static Long xmlImport(XmlInfo m4d, String description)
		throws M4Exception, XmlException
	{
		final M4Info m4Info = m4d.getXmlInfo();
		Long xmlId = null;
		
		String[] array;
		while ( (array = M4Xml.stripOuterTag(description)) != null ) {
			String tag      = array[0];
			String embedded = array[1];
			description     = array[2];
			
			M4InfoEntry entry = m4Info.getInfo(tag);
			if (entry == null) {
				if (tag.equals(XmlInfo.TAG_XML_ID)) {
					xmlId = (Long) M4Xml.createObjectFromXml(XmlInfo.TAG_LONG, embedded);
					// store the object under this id already here,
					// so that it can refer to itself in its following XML tags:
					M4Xml.storeXmlObject(xmlId, m4d);
				}
				else if (tag.equals(XmlInfo.TAG_M4_ID)) {
					if ( ! (m4d instanceof M4Data)) {
						throw new XmlException(
							"Found tag " + tag + " with '" + embedded + "' as the embedded String.\n"
							+ "Problem: The class representing " + tag + " is no sub-class of M4Data,\n"
							+ "so we don't know how to load it from an M4 table!");
					}
					
					Long m4Id = (Long) M4Xml.createObjectFromXml(XmlInfo.TAG_LONG, embedded);
					if (m4Id == null || m4Id.longValue() == 0) {
						throw new XmlException(tag, embedded);	
					}
					((M4Data) m4d).load(m4Id.longValue());
					return xmlId;
				}
				else m4d.importLocal(tag, embedded);
			}
			else {
				String[] innerArray = M4Xml.stripOuterTag(embedded);
				if (innerArray == null) {
					throw new XmlException(
						"Found no information for " + tag
						+ " in embedded description:\n"
						+ embedded + "\n");	
				}
				String innerTag = innerArray[0];
				String innerEmb = innerArray[1];
				Object object  = M4Xml.createObjectFromXml(innerTag, innerEmb);
				
				String setter  = entry.getSetter();
				Class objClass = entry.getTheObjectClass();
				m4d.genericSetter(setter, objClass, object);
			}
		}			
		return xmlId;
	}

	/**
	 * @param xml an XML <code>String</code> to be split into tag and embedded
	 * <code>String</code>
	 * @return a <code>String[3]</code> object with <ul>
	 * <li><code>String[0]</code>: the tag</li>
	 * <li><code>String[1]</code>: the embedded <code>String</code>
	 * <li><code>String[2]</code>: the substring after the closing tag
	 * </ul> if the specified <code>String</code> could be split, and
	 * <code>null</code> otherwise.
	 */
	public static String[] stripOuterTag(String xml) {
		if (xml == null)
			return null;

		int index = xml.indexOf('<');
		if (index != -1) {
			xml = xml.substring(index + 1);
			index = xml.indexOf('>');
			if (index != -1) {
				final String tag = xml.substring(0, index);
				xml = xml.substring(index + 1);
				String closingTag = "</" + tag + ">";
				index = xml.indexOf(closingTag);
				if (index != -1) {
					String embedded = xml.substring(0, index);
					String rest = xml.substring(index + closingTag.length());
					String[] ret = new String[3];
					ret[0] = tag;
					ret[1] = embedded;
					ret[2] = rest.trim();
					return ret;
				}
			}
		}
		return null;
	}

	/**
	 * This is a service method for importing from XML.
	 * For a known tag and a String embedded by the tag an object is
	 * returned.
	 * 
	 * @param tag the name of the tag (case sensitive)
	 * @param embedded the text between the opening and closing tag
	 * @return the deserialized object. <code>null</code> is never returned.
	 * @throws XmlException
	 */	
	public static Object createObjectFromXml(String tag, String embedded)
		throws XmlException
	{
		if (tag == null || embedded == null) {
			throw new XmlException(
				"Null argument found in createObjectFromXml(String tag, String embedded)!");
		}

		if (tag.equals(XmlInfo.TAG_XML_ID)) {
			Long xmlId = (Long) createObjectFromXml(XmlInfo.TAG_LONG, embedded);
			return getXmlObject(xmlId);
		}
		
		if (tag.equals(XmlInfo.TAG_COLLECTION)) {
			Vector ret = new Vector();
			String[] array;
			while ( (array = stripOuterTag(embedded)) != null) {
				String innerTag = array[0];
				String innerEmb = array[1];
				embedded = array[2];
				Object obj = createObjectFromXml(innerTag, innerEmb);
				if (obj != null) {
					ret.add(obj);
				}
			}
			return ret;
		}
		
		if (tag.equals(XmlInfo.TAG_STRING)) {
			return M4Xml.unEscapeXmlString(embedded);
		}
		
		// trim() is necessary to avoid parse exception below!
		embedded = embedded.trim();
	
		if (tag.equals(XmlInfo.TAG_LONG)) {
			return new Long(embedded);
		}
		if (tag.equals(XmlInfo.TAG_INTEGER)) {
			return new Integer(embedded);
		}
		if (tag.equals(XmlInfo.TAG_SHORT)) {
			return new Short(embedded);
		}
		
		throw new XmlException(tag, embedded);
	}

	// *** Service method to escape and un-escape reserved XML chars in Strings ***
	
	private static String escapeXmlString(String text) {
		StringBuffer sbuf = new StringBuffer();
		while (text != null && text.length() > 0) {
			int index =
				min(text.indexOf('<'), text.indexOf('>'), text.indexOf('&'));
			if (index > -1) {
				char c = text.charAt(index);
				String replaceWith;
				switch (c) {
					case '<': replaceWith = "&lt;"; break;
					case '>': replaceWith = "&gt;"; break;
					default:  replaceWith = "&amp;";
				}
				sbuf.append(text.substring(0, index));
				sbuf.append(replaceWith);
				text = text.substring(index + 1);
			}
			else {
				sbuf.append(text);
				text = null;
			}
		}	
		return sbuf.toString();		
	}
	
	private static String unEscapeXmlString(String text) {
		StringBuffer sbuf = new StringBuffer();
		while (text != null && text.length() > 0) {
			int index =
				min(text.indexOf("&lt;"), text.indexOf("&gt;"), text.indexOf("&amp;"));
			if (index > -1) {
				char c = text.charAt(index + 1);
				char replaceWith;
				switch (c) {
					case 'l': replaceWith = '<'; break;
					case 'g': replaceWith = '>'; break;
					default:  replaceWith = '&';
				}
				sbuf.append(text.substring(0, index));
				try{
					sbuf.append(replaceWith);
					if (false) throw new IOException(); // bit of a hack...
				}catch(IOException error){
					error.printStackTrace();
				}
				index = text.indexOf(';', index) + 1;
				text = text.substring(index);
			}
			else {
				sbuf.append(text);
				text = null;
			}
		}	
		return sbuf.toString();		
	}

	private static int min(int a, int b, int c) {
		int minab = (a < b || b == -1) ? a : b;
		return ((minab < c || c == -1) ? minab : c);
	}

}
/*
 * Historie
 * --------
 * 
 * $Log: M4Xml.java,v $
 * Revision 1.5  2006/04/11 14:10:10  euler
 * Updated license text.
 *
 * Revision 1.4  2006/04/06 16:31:09  euler
 * Prepended license remark.
 *
 * Revision 1.3  2006/04/03 09:32:53  euler
 * Improved Export/Import of Projections
 * and Subconcept links.
 *
 * Revision 1.2  2006/01/12 11:33:11  euler
 * Changed the way coordinates are stored completely.
 *
 * Revision 1.1  2006/01/03 09:54:03  hakenjos
 * Initial version!
 *
 */
