/* IDOMs.java


Purpose:
Description:
History:
	2002/01/08 10:37:16, Create, Tom M. Yeh

Copyright (C) 2001 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.idom.util;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.idom.Attribute;
import org.zkoss.idom.Document;
import org.zkoss.idom.Element;
import org.zkoss.idom.Group;
import org.zkoss.idom.Item;
import org.zkoss.idom.Text;
import org.zkoss.idom.transform.Transformer;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.mesg.MCommon;
import org.zkoss.util.IllegalSyntaxException;

/**
 * The iDOM relevant utilities.
 *
 * @author tomyeh
 * @see org.zkoss.idom.Item
 * @see org.zkoss.idom.Group
 */
public class IDOMs {
	private static final Logger log = LoggerFactory.getLogger(IDOMs.class);

	/** Returns the required element.
	 * @param elemnm the element name
	 */
	public static final Element getRequiredElement(Element e, String elemnm)
	throws IllegalSyntaxException {
		final Element sub = e.getElement(elemnm);
		if (sub == null)
			throw new IllegalSyntaxException(
				MCommon.XML_ELEMENT_REQUIRED, new Object[] {elemnm, e.getLocator()});
		return sub;
	}
	/** Returns the required element value.
	 * <p>Note: the returned value may be an empty string (if the element
	 * contains no text at all).
	 * @exception IllegalSyntaxException if the element is not found
	 */
	public static final String getRequiredElementValue(Element e, String elemnm)
	throws IllegalSyntaxException {
		final Element sub = e.getElement(elemnm);
		if (sub == null)
			throw new IllegalSyntaxException(
				MCommon.XML_ELEMENT_REQUIRED, new Object[] {elemnm, e.getLocator()});
		return sub.getText(true);
	}
	/** Returns the required attribute value.
	 * @exception IllegalSyntaxException if the element is not found
	 */
	public static final String getRequiredAttributeValue(Element e, String attrnm)
	throws IllegalSyntaxException {
		final Attribute attr = e.getAttributeItem(attrnm);
		if (attr == null)
			throw new IllegalSyntaxException(
				MCommon.XML_ATTRIBUTE_REQUIRED, new Object[] {attrnm, e.getLocator()});
		return attr.getValue();
	}
	/** Returns the first child element, or null if no child element at all.
	 */
	public static final Element getFirstElement(Group group) {
		final Iterator it = group.getElements().iterator();
		return it.hasNext() ? (Element)it.next(): null;
	}

	/** Returns the first element whose sub-element called "name" has the
	 * same content as the name argument, or null if not found.
	 *
	 * @param elems a list of elements to look for the specified name
	 */
	public static final Element findElement(List elems, String name) {
		for (final Iterator it = elems.iterator(); it.hasNext();) {
			final Element e = (Element)it.next();
			if (Objects.equals(name, e.getElementValue("name", true)))
				return e;
		}
		return null;
	}

	/** Parses a tree of parameter elements into a map.
	 *
	 * <p>The tree of parameter elements is as follows.
	 * <pre><code>
	 * &lt;type&gt;
	 *   &lt;name&gt;any&lt;/name&gt;
	 *   &lt;vaue&gt;any&lt;/vaue&gt;
	 * &lt;/type&gt;
	 *
	 * @return the map after parsed (never null). An empty map is returned
	 * if no parameter is defined. The map is in the same order of the element tree.
	 */
	public static final
	Map<String, String> parseParams(Element elm, String type, String name, String value) {
		final Map<String, String> map = new LinkedHashMap<String, String>();
		for (Iterator it = elm.getElements(type).iterator(); it.hasNext();) {
			final Element el = (Element)it.next();
			final String nm = getRequiredElementValue(el, name);
			final String val = getRequiredElementValue(el, value);
			map.put(nm, val);
		}
		return map;
	}
	
	/** Formats the specified element for better readability by
	 * adding white spaces.
	 */
	public static void format(Element e) {
		//add proper spacing between consecutive elements
		boolean elemFound = true;
		for (final ListIterator<Item> it = e.getChildren().listIterator(); it.hasNext();) {
			final Object o = it.next();
			if (o instanceof Element) {
				if (elemFound) { //insert space
					it.previous();
					it.add(new Text("\n\t"));
					it.next();
				} else {
					elemFound = true;
				}

				format((Element)o); //recursive
			} else {
				elemFound = false;
			}
		}
	}

	/** Converts elements to their contents if the giving object is
	 * an element or an array or a collection of elements.
	 * One item of an collection might be another collection or array.
	 */
	@SuppressWarnings("unchecked")
	public static final Object toContents(Object obj) {
		if (obj instanceof Collection) {
			Collection c = (Collection)obj;
			boolean cvted = false;
			Collection rets = new LinkedList();
			for (Iterator it = c.iterator(); it.hasNext();) {
				Object o = it.next();
				Object o2 = toContents(o); //recursive
				if (o != o2)
					cvted = true;
				rets.add(o2);
			}

			if (cvted)
				return rets;
		} else if (obj instanceof Object[]) {
			Object[] ary = (Object[])obj;
			boolean cvted = false;
			Object[] rets = new Object[ary.length];
			for (int j = 0; j < ary.length; ++j) {
				Object o2 = toContents(ary[j]); //recursive
				if (ary[j] != o2)
					cvted = true;
				rets[j] = o2;
			}
			if (cvted)
				return rets;
		} else if (obj instanceof Element) {
			return ((Element)obj).getContent();
		}
		return obj;
	}
	/** Set the contents of elements.
	 * The val argument could be an array and a collection, and each
	 * item will be assigned to each of the list one-by-one.
	 *
	 * <p>Unlike {@link #toContents}, it handles only a collection of elements
	 * -- all items must be elements.
	 *
	 * @param elems the collection of elements
	 * @param val the value which could be an object, an array or a collection
	 */
	public static final void setContents(Collection<Element> elems, Object val) {
		Object[] ary = null;
		if (val instanceof Object[]) {
			ary = (Object[])val;
		} else if (val instanceof Collection) {
			ary = ((Collection)val).toArray();
		} else {
			ary = new Object[] {val};
		}

		Iterator<Element> it = elems.iterator();
		for (int j = 0; it.hasNext(); ++j) {
			it.next().setContent(j < ary.length ? ary[j]: null);
		}
	}

	/** Transforms a document to a string.
	 * The string is XML correct.
	 */
	public final static String toString(Document doc)
	throws TransformerConfigurationException, TransformerException {
		final StringWriter writer = new StringWriter();
		new Transformer().transform(doc, new StreamResult(writer));
		return writer.toString();
	}
	/**
	 * Print a readable tree of the specified group to System.out.
	 * It is for debug purpose and the generated format is <i>not</i> XML.
	 * To generate XML, uses {@link Transformer} or {@link #toString}.
	 * @see #toString
	 */
	public static final void dumpTree(Group group) {
		dumpTree(System.out, group);
	}
	/**
	 * Print a readable tree of the specified group to the specified stream.
	 * It is for debug purpose and the generated format is <i>not</i> XML.
	 * To generate XML, uses {@link Transformer} or {@link #toString}.
	 * @see #toString
	 */
	public static final void dumpTree(PrintStream s, Group group) {
		dumpTree(new PrintWriter(s, true), group);
	}
	/**
	 * Print a readable tree of the specified group to the specified writer.
	 * It is for debug purpose and the generated format is <i>not</i> XML.
	 * To generate XML, uses {@link Transformer} or {@link #toString}.
	 * @see #toString
	 */
	public static final void dumpTree(PrintWriter s, Group group) {
		dumpTree(s, group, "");
	}
	private static final void
	dumpTree(PrintWriter s, Item vtx, String prefix) {
		s.print(prefix);
		s.print(vtx);
		if (vtx instanceof Group) {
			prefix = prefix + "  ";
			for (Iterator it = ((Group)vtx).getChildren().iterator();
			it.hasNext();)
				dumpTree(s, (Item)it.next(), prefix);
		}
	}

	/** Returns whether the loaded document's version is correct.
	 *
	 * <p>It assumes the version info is specified in the document in
	 * the following format:
	 *
	 * <pre></code>
&lt;version&gt;
	&lt;version-class&gt;org.zkoss.zul.Version&lt;/version-class&gt;
	&lt;version-uid&gt;3.0.0&lt;/version-uid&gt;
&lt;/version&gt;
</code></pre>
	 *
	 * <p>Note: it returns true if the version info is not found.
	 *
	 * @param doc the document to check
	 * @param url the URL used to show the readable message if the
	 * version doesn't match
	 * @since 3.0.0
	 */
	public static boolean checkVersion(Document doc, URL url)
	throws Exception {
		final Element el = doc.getRootElement().getElement("version");
		if (el != null) {
			final String clsnm = IDOMs.getRequiredElementValue(el, "version-class");
			final String uid = IDOMs.getRequiredElementValue(el, "version-uid");
			final Class cls = Classes.forNameByThread(clsnm);
			final Field fld = cls.getField("UID");
			final String uidInClass = (String)fld.get(null);
			if (uid.equals(uidInClass)) {
				return true;
			} else {
				log.info("Ignore "+url+"\nCause: version not matched; expected="+uidInClass+", xml="+uid);
				return false;
			}
		} else {
			return true;
		}
	}
}
