/*
 *    GSXML.java
 *    Copyright (C) 2008 New Zealand Digital Library, http://www.nzdl.org
 *
 *    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; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.greenstone.gsdl3.util;

import java.io.Serializable;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.log4j.Logger;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import org.greenstone.gsdl3.util.MyNodeList;
import org.greenstone.util.Misc;

/** various functions for extracting info out of GS XML */
public class GSXML
{

	static Logger logger = Logger.getLogger(org.greenstone.gsdl3.util.GSXML.class.getName());

	// greenstone message xml elements
	public static final String MESSAGE_ELEM = "message";
	public static final String REQUEST_ELEM = "request";
	public static final String RESPONSE_ELEM = "response";
	public static final String COLLECTION_ELEM = "collection";
  public static final String CC_SERVICES_ELEM = "ccServices";
	public static final String SERVICE_ELEM = "service";
	public static final String CLUSTER_ELEM = "serviceCluster";
	public static final String SITE_ELEM = "site";
	public static final String PARAM_ELEM = "param";
	public static final String PARAM_OPTION_ELEM = "option";
        public static final String LIBRARY_PARAM_ELEM = "libraryParam";
	public static final String CONTENT_ELEM = "content";
	public static final String RESOURCE_ELEM = "resource";
	public static final String DOCUMENT_ELEM = "document";
	public static final String METADATA_ELEM = "metadata";
	public static final String SERVICE_CLASS_ELEM = "serviceRack";
	public static final String CLASSIFIER_ELEM = "classifier";
	public static final String APPLET_ELEM = "applet";
	public static final String APPLET_DATA_ELEM = "appletData";
	public static final String CONFIGURE_ELEM = "configure";
	public static final String STATUS_ELEM = "status";
	public static final String ERROR_ELEM = "error";
	public static final String DEFAULT_ELEM = "default";
	public static final String STYLESHEET_ELEM = "format";//"stylesheet"; // any additional stylesheet stuff is carried in the message inside this elem
	public static final String FORMAT_ELEM = "format"; // config files use format - should we use this instead of stylesheet??
	public static final String GLOBAL_FORMAT_ELEM = "globalFormat";
	public static final String TERM_ELEM = "term";
	public static final String STOPWORD_ELEM = "stopword";
	public static final String FACET_ELEM = "facet";
	public static final String COUNT_ELEM = "count";
	public static final String SYSTEM_ELEM = "system";
	public static final String FORMAT_STRING_ELEM = "formatString";
	public static final String EXTRA_METADATA = "extraMetadata";
	public static final String PATH_ELEM = "path";

	//config file elems
	public static final String COLLECTION_CONFIG_ELEM = "collectionConfig";
	public static final String COLLECTION_BUILD_ELEM = "buildConfig";
	public static final String COLLECTION_INIT_ELEM = "collectionInit";
	public static final String RECOGNISE_ELEM = "recognise";
	public static final String DOC_TYPE_ELEM = "docType";
	public static final String SEARCH_ELEM = "search";
	public static final String INFODB_ELEM = "infodb";
	public static final String INDEX_ELEM = "index";
	public static final String INDEX_STEM_ELEM = "indexStem";
	public static final String INDEX_OPTION_ELEM = "indexOption";
	public static final String PARAM_DEFAULT_ELEM = "paramDefault";
	public static final String BROWSE_ELEM = "browse";
	public static final String DISPLAY_ELEM = "display";
	public static final String LEVEL_ELEM = "level";
	public static final String REPLACE_ELEM = "replace";
        public static final String EXTRA_INFO_ELEM = "extraInfo";
	public static final String DBINFO_ELEM = "dbInfo";
	public static final String DBNAME_ATT = "dbname";
	public static final String DBPATH_ATT = "dbpath";
	public static final String SQLSTATE_ATT = "sqlstate";
	public static final String DATABASE_TYPE_ELEM = "databaseType";
	public static final String SHORTNAME_ATT = "shortname";
	public static final String NOTIFY_ELEM = "notify";
	public static final String NOTIFY_HOST_ATT = "host";
  public static final String SERVICE_ATT = "service";
        //doc.xml file elems
	public static final String DOCXML_SECTION_ELEM = "Section";
	public static final String DOCXML_DESCRIPTION_ELEM = "Description";
	public static final String DOCXML_METADATA_ELEM = "Metadata";
	public static final String DOCXML_CONTENT_ELEM = "Content";

	// elems for the pages to be processed by xslt
	public final static String PAGE_ELEM = "page";
	public final static String CONFIGURATION_ELEM = "config";
	public final static String PAGE_REQUEST_ELEM = "pageRequest";
	public final static String PAGE_RESPONSE_ELEM = "pageResponse";
	public final static String PAGE_EXTRA_ELEM = "pageExtra";

	//public final static String DESCRIPTION_ELEM = "description";

	public static final String ACTION_ELEM = "action";
	public static final String SUBACTION_ELEM = "subaction";

	// add on to another elem type to get a list of that type
	public static final String LIST_MODIFIER = "List";
	// used to refer back to another element type
	public static final String REF_MODIFIER = "Ref";
	// greenstone xml attributes
	public static final String COLLECTION_ATT = "collection";
	public static final String NAME_ATT = "name";
	public static final String MATCH_ATT = "match";
	public static final String TO_ATT = "to";
	public static final String USER_ID_ATT = "uid";
	public static final String FROM_ATT = "from";
	public static final String LANG_ATT = "lang";
  public static final String KEY_ATT = "key";
  public static final String DICTIONARY_ATT = "dictionary";
	public static final String HREF_ATT = "href";
	public static final String TYPE_ATT = "type";
	public static final String DB_TYPE_ATT = "dbType";
	public static final String VALUE_ATT = "value";
	public static final String DEFAULT_ATT = "default";
	public static final String INFO_ATT = "info";
	public static final String ACTION_ATT = "action";
	public static final String SUBACTION_ATT = "subaction";
	public static final String OUTPUT_ATT = "output";
	public static final String ADDRESS_ATT = "address";
	public static final String LOCAL_SITE_ATT = "localSite";
	public static final String LOCAL_SITE_NAME_ATT = "localSiteName";
	public static final String STATUS_ERROR_CODE_ATT = "code";
	public static final String STATUS_PROCESS_ID_ATT = "pid";
	public static final String PARAM_SHORTNAME_ATT = "shortname";
	public static final String PARAM_IGNORE_POS_ATT = "ignore";
	public static final String CLASSIFIER_CONTENT_ATT = "content";
	public static final String ERROR_TYPE_ATT = "type";
	public static final String COLLECT_TYPE_ATT = "ct";
	public static final String HIDDEN_ATT = "hidden";
	public static final String FACET_ATT = "facet";
	public static final String PATH_ATT = "path";
  public static final String SENSITIVE_ATT = "sensitive";
  public static final String NO_ARCHIVES_ATT = "no_archives";
	// document stuff
	public static final String DOC_TYPE_ATT = "docType";
        public static final String DOC_VERSION_ATT = "docVersion";  // File-Level Document-Version History _fldv_history
	public static final String DOC_NODE_ELEM = "documentNode";
	public static final String NODE_CONTENT_ELEM = "nodeContent";
	public static final String NODE_STRUCTURE_ELEM = "nodeStructure";
	public static final String NODE_ID_ATT = "nodeID";
	public static final String HREF_ID_ATT = "hrefId"; // for ids that need translating
	public static final String HL_SNIPPET_ELEM = "highlightSnippet";
	public static final String ID_MOD_ATT = "idMod"; // might hold .rt etc for hrefIds
	public static final String NODE_OID = "oid";
	public static final String NODE_NAME_ATT = "nodeName";
	public static final String NODE_TYPE_ATT = "nodeType";
	public static final String NODE_RANK_ATT = "rank";
	public static final String NODE_TYPE_ROOT = "root";
	public static final String NODE_TYPE_INTERNAL = "internal";
	public static final String NODE_TYPE_LEAF = "leaf";

	public static final String DOC_TYPE_SIMPLE = "simple";
	public static final String DOC_TYPE_PAGED = "paged";
	public static final String DOC_TYPE_HIERARCHY = "hierarchy";
	public static final String DOC_TYPE_PAGED_HIERARCHY = "pagedhierarchy";

	public static final String SESSION_EXPIRATION = "session_expiration";

	// classifier stuff
	public static final String CLASS_NODE_ELEM = "classifierNode";
	public static final String CLASS_NODE_ORIENTATION_ATT = "orientation";

	// parameter types
	public static final String PARAM_TYPE_INTEGER = "integer";
	public static final String PARAM_TYPE_BOOLEAN = "boolean";
	public static final String PARAM_TYPE_ENUM_START = "enum";
	public static final String PARAM_TYPE_ENUM_SINGLE = "enum_single";
	public static final String PARAM_TYPE_ENUM_MULTI = "enum_multi";
	public static final String PARAM_TYPE_STRING = "string";
	public static final String PARAM_TYPE_TEXT = "text";
	public static final String PARAM_TYPE_MULTI = "multi";
	public static final String PARAM_TYPE_FILE = "file";
	public static final String PARAM_TYPE_INVISIBLE = "invisible";
	public static final String PARAM_TYPE_CHECKBOX_LIST = "checkbox_list";
	// stuff for text strings
	public static final String DISPLAY_TEXT_ELEM = "displayItem";
	// the following are used for the name attributes
	public static final String DISPLAY_TEXT_NAME = "name";
	public static final String DISPLAY_TEXT_SUBMIT = "submit";
	public static final String DISPLAY_TEXT_DESCRIPTION = "description";

	// request types
	// get the module description
	public static final String REQUEST_TYPE_DESCRIBE = "describe";
	// startup a process
	public static final String REQUEST_TYPE_PROCESS = "process";
	// get the status of an ongoing process
	public static final String REQUEST_TYPE_STATUS = "status";
	// system type request - eg reload a collection
	public static final String REQUEST_TYPE_SYSTEM = "system";
	// page requests to the Receptionist/Actions
	public static final String REQUEST_TYPE_PAGE = "page"; // used to be cgi
	// get any format info for a service
	public static final String REQUEST_TYPE_FORMAT = "format";
	// modify the requests
	public static final String REQUEST_TYPE_MESSAGING = "messaging";
	// save the format string
	public static final String REQUEST_TYPE_FORMAT_STRING = "formatString";
	// check credentials
	public static final String REQUEST_TYPE_SECURITY = "security";

	// service types
	public static final String SERVICE_TYPE_QUERY = "query";
	public static final String SERVICE_TYPE_RETRIEVE = "retrieve";
	public static final String SERVICE_TYPE_BROWSE = "browse";
	public static final String SERVICE_TYPE_GROUPINFO = "groupinfo";
	public static final String SERVICE_TYPE_APPLET = "applet";
	public static final String SERVICE_TYPE_PROCESS = "process";
	public static final String SERVICE_TYPE_ENRICH = "enrich";
	public static final String SERVICE_TYPE_OAI = "oai";
	public static final String FLAX_PAGE = "flaxPage";
	public static final String FLAX_PAGE_GENERATION = "FlaxPageGeneration";

	// system command types and attributes
	public static final String SYSTEM_TYPE_CONFIGURE = "configure";
	public static final String SYSTEM_TYPE_ACTIVATE = "activate";
	public static final String SYSTEM_TYPE_DEACTIVATE = "deactivate";
	public static final String SYSTEM_TYPE_PING = "ping";
	public static final String SYSTEM_TYPE_AUTHENTICATED_PING = "authenticated-ping";
	//public static final String SYSTEM_TYPE_ISPERSISTENT = "is-persistent";

	public static final String SYSTEM_SUBSET_ATT = "subset";
	public static final String SYSTEM_MODULE_TYPE_ATT = "moduleType";
	public static final String SYSTEM_MODULE_NAME_ATT = "moduleName";

	// communicator types
	public static final String COMM_TYPE_SOAP_JAVA = "soap";

	// error types
	public static final String ERROR_TYPE_SYNTAX = "syntax";
	public static final String ERROR_TYPE_SYSTEM = "system";
	public static final String ERROR_TYPE_INVALID_ID = "invalid_id";
	public static final String ERROR_TYPE_OTHER = "other";

	// some system wide param names
	public static final String SUBSET_PARAM = "subset";

	//for plugin
	public static final String PLUGIN_ELEM = "plugin";
	public static final String IMPORT_ELEM = "import";

	//for authentication
	public static final String AUTHEN_NODE_ELEM = "authenticationNode";
	public static final String USER_NODE_ELEM = "userNode";

	//for configure action results
	public static final String SUCCESS = "success";
	public static final String ERROR = "error";

	//security tags and attributes
	public static final String SECURITY_ELEM = "security";
        public static final String DISABLED_ATT = "disabled";
	public static final String SCOPE_ATT = "scope";
        public static final String SCOPE_COLLECTION = "collection";
        public static final String SCOPE_DOCUMENT = "document";
	public static final String DEFAULT_ACCESS_ATT = "default_access";
        public static final String ACCESS_PUBLIC = "public";
         public static final String ACCESS_PRIVATE = "private";
        public static final String VERIFY_ATT = "humanVerify";
        public static final String SITE_KEY_ATT = "siteKey";
        public static final String SECRET_KEY_ATT = "secretKey";
        public static final String USE_RECAPTCHA_ATT = "useRecaptcha";
	public static final String EXCEPTION_ELEM = "exception";
	public static final String DOCUMENT_SET_ELEM = "documentSet";
	public static final String GROUP_ELEM = "group";
	public static final String MATCH_ELEM = "match";
	public static final String FIELD_ATT = "field";
	public static final String USER_INFORMATION_ELEM = "userInformation";
	public static final String USERNAME_ATT = "username";
	public static final String GROUPS_ATT = "groups";
    public static final String EDIT_ENABLED_ATT = "editEnabled";
	public static final String BASE_URL = "baseURL";

	// only for authenticated-ping
	public static final String PASSWORD_ATT = "password";
	
	//for classifiers
	public static final String CHILD_TYPE_ATT = "childType";
	public static final String CLASSIFIER_STYLE_ATT = "classifierStyle";
	public static final String HLIST = "HList";
	public static final String VLIST = "VList";

	//for namespaces
	public static final String XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform";
	public static final String GSF_NAMESPACE = "http://www.greenstone.org/greenstone3/schema/ConfigFormat";
  public static final String GSLIB_NAMESPACE = "http://www.greenstone.org/skinning";
	public static final String JAVA_NAMESPACE = "http://xml.apache.org/xslt/java";
	public static final String UTIL_NAMESPACE = "xalan://org.greenstone.gsdl3.util.XSLTUtil";

  // all the namespaces in a string for easy addition to a string
  public  static final String ALL_NAMESPACES_ATTS = "xmlns:gsf='" + GSF_NAMESPACE + "' xmlns:xsl='" + XSL_NAMESPACE + "' xmlns:gslib='" + GSLIB_NAMESPACE + "' xmlns:java='" + JAVA_NAMESPACE + "' xmlns:util='" + UTIL_NAMESPACE +"' ";
  public static final String STD_NAMESPACES_ATTS = "xmlns:gsf='" + GSF_NAMESPACE + "' xmlns:xsl='" + XSL_NAMESPACE + "' xmlns:gslib='" + GSLIB_NAMESPACE +"' ";

	//Groups configuration
	public static final String HIERARCHY_ELEM = "hierarchy";
	public static final String GROUP_DESC_ELEM = "groupDescriptions";
	public static final String TITLE_ELEM = "title";
	public static final String DESCRIPTION_ELEM = "description";
	public static final String SHORT_DESCRIPTION_ELEM = "shortDescription";
	public static final String BACKGROUND_IMAGE_ELEM = "backgroundImage";
	public static final String POSITION_ATT = "position";
	
	/**
	 * takes a list of elements, and returns an array of strings of the values
	 * of attribute att_name
	 */
	public static String[] getAttributeValuesFromList(Element list, String att_name)
	{

		NodeList children = list.getChildNodes();

		int num_nodes = children.getLength();
		String[] ids = new String[num_nodes];
		for (int i = 0; i < num_nodes; i++)
		{
			Element e = (Element) children.item(i);
			String id = e.getAttribute(att_name);
			ids[i] = id;
		}

		return ids;
	}

      public static HashMap<String, String> getParamMap(NodeList params) {
      HashMap<String, String> map = new HashMap<String, String>();
      for(int i=0; i<params.getLength(); i++) {
        Element param = (Element)params.item(i);
        String param_name = param.getAttribute(NAME_ATT);
        String param_value = param.getAttribute(VALUE_ATT);
        map.put(param_name, param_value);
      }
      return map;
    }

	public static HashMap<String, Serializable> extractParams(Element xml, boolean deep)
	{
		return extractParams(xml, deep, null);
	}

	/**
	 * takes a paramList element, and gets a HashMap of name-value pairs if
	 * deep=true, extracts embedded params, otherwise just top level params
	 */
	public static HashMap<String, Serializable> extractParams(Element xml, boolean deep, String toFind)
	{
		if (xml == null)
		{
			return null;
		}

		if (!xml.getNodeName().equals(PARAM_ELEM + LIST_MODIFIER))
		{
			logger.error("paramList element should have been passed to extractParams, instead it was " + xml.getNodeName());
			return null;
		}

		NodeList params = null;
		if (deep)
		{ // get all the nested ones
			params = xml.getElementsByTagName(PARAM_ELEM);
		}
		else
		{ // just get the top  level ones
			params = xml.getChildNodes();
		}
		HashMap<String, Serializable> param_map = new HashMap<String, Serializable>();
		for (int i = 0; i < params.getLength(); i++)
		{
			if (params.item(i).getNodeName().equals(PARAM_ELEM))
			{
				Element param = (Element) params.item(i);
				String name = param.getAttribute(NAME_ATT);
				String value = getValue(param); //att or content

				// For only one parameter
				if (toFind != null && name.equals(toFind))
				{
					param_map.put(name, value);
					return param_map;
				}
				else if (toFind != null)
					continue;

				int pos = name.indexOf('.');
				if (pos == -1)
				{ // a base param
					param_map.put(name, value);
				}
				else
				{ // a namespaced param
                                  // what if we have a param already of the namespace name, eg p???
					String namespace = name.substring(0, pos);
					name = name.substring(pos + 1);
					HashMap<String, String> map = (HashMap<String, String>) param_map.get(namespace);
					if (map == null)
					{
						map = new HashMap<String, String>();
						param_map.put(namespace, map);
					}
					map.put(name, value);
				}
			}
		}
		return param_map;
	}

	/** gets the value att or the text content */
	public static String getValue(Element e)
	{
		String val = e.getAttribute(VALUE_ATT);
		if (val == null || val.equals(""))
		{
			// have to get it out of the text
			val = getNodeText(e);

		}
		else
		{
			// unescape the xml stuff
			val = unXmlSafe(val);
		}
		return val;
	}

	/** extracts the text out of a node */
	public static Node getNodeTextNode(Element param)
	{
		param.normalize();
		Node n = param.getFirstChild();
		while (n != null && n.getNodeType() != Node.TEXT_NODE)
		{
			n = n.getNextSibling();
		}
		return n;
	}

	/** extracts the text out of a node */
	public static String getNodeText(Element param)
	{
		Node text_node = getNodeTextNode(param);
		if (text_node == null)
		{
			return "";
		}
		return text_node.getNodeValue();
	}

	public static void setNodeText(Element elem, String text)
	{
		Node old_text_node = getNodeTextNode(elem);
		if (old_text_node != null)
		{
			elem.removeChild(old_text_node);
		}
		Text t = elem.getOwnerDocument().createTextNode(text);
		elem.appendChild(t);
	}

	/** add text to a document/subsection element */
	public static boolean addDocText(Element doc, String text)
	{
	  
	  Element content = doc.getOwnerDocument().createElement(NODE_CONTENT_ELEM);
		Text t = doc.getOwnerDocument().createTextNode(text);
		content.appendChild(t);
		doc.appendChild(content);
		return true;
	}

	/** add an error message, unknown error type */
  public static boolean addError(Element doc, String text)
	{
		return addError(doc, text, ERROR_TYPE_OTHER);
	}

	/** add an error message */
	public static boolean addError(Element doc, String text, String error_type)
	{

		Element content = doc.getOwnerDocument().createElement(ERROR_ELEM);
		content.setAttribute(ERROR_TYPE_ATT, error_type);
		Text t = doc.getOwnerDocument().createTextNode(text);
		content.appendChild(t);
		doc.appendChild(content);
		return true;
	}

	/** add an error message */
	public static boolean addError(Element doc, Throwable error)
	{
		return addError(doc, error, ERROR_TYPE_OTHER);
	}

	/** add an error message */
	public static boolean addError(Element doc, Throwable error, String error_type)
	{
		error.printStackTrace();
		return addError(doc, error.toString(), error_type);
	}

  public static boolean addGSNamespaces(Element elem, boolean include_extension_ns) {
    if (elem == null) {
      return false;
    }
    elem.setAttribute("xmlns:gsf", GSF_NAMESPACE);
    elem.setAttribute("xmlns:xsl", XSL_NAMESPACE);
    elem.setAttribute("xmlns:gslib", GSLIB_NAMESPACE);
    if (include_extension_ns) {
      elem.setAttribute("xmlns:util", UTIL_NAMESPACE);
      elem.setAttribute("xmlns:java", JAVA_NAMESPACE);
    }
    return true;
  }

  /** This adds any namespace attributes from donor_element into main_elem if they are not already there */
  public static void addMissingNamespaceAttributes(Element main_elem, Element donor_elem) {
    // are there any new namespaces to be added?
    NamedNodeMap attributes = donor_elem.getAttributes();
    for (int i=0; i<attributes.getLength(); i++) {
      Attr att = (Attr)attributes.item(i);
      String att_name = att.getName();
      if (att_name.startsWith("xmlns:")) {
        //logger.error("found a namespace att "+att_name);
        if (main_elem.getAttribute(att_name).equals("")) {
          main_elem.setAttribute(att_name, att.getValue());
          //logger.error("setting att in main");
        }
      }
    }
    
    
  }

  public static void removeAllAttributes(Element elem) {

    while (elem.getAttributes().getLength() > 0) {
      Node att = elem.getAttributes().item(0);
      elem.removeAttribute(att.getNodeName());
    }    
    
  }
	public static Element createMetadataParamList(Document owner, Vector meta_values)
	{

		Element meta_param_list = owner.createElement(PARAM_ELEM + LIST_MODIFIER);
		Iterator i = meta_values.iterator();
		while (i.hasNext())
		{
			String next = (String) i.next();
			Element meta_param = owner.createElement(PARAM_ELEM);
			meta_param_list.appendChild(meta_param);
			meta_param.setAttribute(NAME_ATT, "metadata");
			meta_param.setAttribute(VALUE_ATT, next);
		}
		return meta_param_list;
	}

	/** adds a metadata elem to a list */
	public static boolean addMetadata(Element list, String meta_name, String meta_value)
	{
		if (meta_value == null || meta_value.equals(""))
		{
			return false;
		}
		Document owner = list.getOwnerDocument();
		Element data = owner.createElement(METADATA_ELEM);
		data.setAttribute(NAME_ATT, meta_name);
		Text t = owner.createTextNode(meta_value);
		data.appendChild(t);
		list.appendChild(data);
		return true;

	}

	/**
	 * copies the metadata out of the metadataList of 'from' into the
	 * metadataList of 'to'
	 */
  public static boolean mergeMetadataLists(Node to, Node from) {
    return mergeSpecifiedLists(to, from, METADATA_ELEM);
  }

  /** copies metadata from meta_list into the metadataList of to **/
  public static boolean mergeMetadataFromList(Node to, Node meta_list) {
    return mergeSpecifiedFromList(to, meta_list, METADATA_ELEM);
  }


  public static boolean mergeSpecifiedLists(Node to, Node from, String element_name) {
    // find the list in the from element
    Node from_list = getChildByTagName(from, element_name+LIST_MODIFIER);
    if (from_list == null) {
      // nothing to copy
      return false;
    }
   
    return mergeSpecifiedFromList(to, from_list, element_name);
  }
    public static boolean mergeSpecifiedFromList(Node to, Node from_list, String element_name) {
      if (from_list == null) {
	return false;
      }
      Document to_owner = to.getOwnerDocument();
      Node to_list = getChildByTagName(to, element_name+LIST_MODIFIER);
      if (to_list == null) {
	// just copy over the whole list
	to.appendChild(to_owner.importNode(from_list, true));
	return true;
      }
      // otherwise we copy all the elements
      NodeList from_items = ((Element) from_list).getElementsByTagName(element_name);

      for (int i = 0; i < from_items.getLength(); i++)
	{
	  to_list.appendChild(to_owner.importNode(from_items.item(i), true));
	}
      return true;
      
    }
  
  /** copies all the children from from to to */
  public static boolean mergeElements(Element to, Element from)
	{

		Document owner = to.getOwnerDocument();
		Node child = from.getFirstChild();
		while (child != null)
		{
			to.appendChild(owner.importNode(child, true));
			child = child.getNextSibling();
		}
		return true;
	}

	/** returns the (first) element child of the node n */
	public static Element getFirstElementChild(Node n)
	{

		Node child = n.getFirstChild();
		while (child != null)
		{
			if (child.getNodeType() == Node.ELEMENT_NODE)
			{
				return (Element) child;
			}
			child = child.getNextSibling();
		}
		return null; //no element child found
	}

	/** returns the (first) child element with the given name */
	public static Node getChildByTagName(Node n, String name)
	{
		if (n != null)
		{ // this line is an attempted solution to the NullPointerException mentioned 
			// in trac bug ticket #225. If n is null can't do n.getFirstChild() below. As per bug #225: 
			// getNodeByPath() is called by GS2BrowseAction, which then calls this method.
			// If n is null, null will be returned which GS2BrowseAction already checks for. It's here
			// that the NullPointerException was thrown.

			Node child = n.getFirstChild();
			while (child != null)
			{
				if (child.getNodeName().equals(name))
				{
					return child;
				}
				child = child.getNextSibling();
			}
		}
		return null; //not found
	}

	/**
	 * returns the (nth) child element with the given name index numbers start
	 * at 0
	 */
	public static Node getChildByTagNameIndexed(Node n, String name, int index)
	{
		if (index == -1)
		{
			return getChildByTagName(n, name);
		}
		int count = 0;
		Node child = n.getFirstChild();
		while (child != null)
		{
			if (child.getNodeName().equals(name))
			{
				if (count == index)
				{
					return child;
				}
				else
				{
					count++;
				}
			}
			child = child.getNextSibling();
		}
		return null; //not found
	}

	/**
	 * takes an xpath type expression of the form name/name/... and returns the
	 * first node that matches, or null if not found
	 */
	public static Node getNodeByPath(Node n, String path)
	{

		String link = GSPath.getFirstLink(path);
		path = GSPath.removeFirstLink(path);
		while (!link.equals(""))
		{
			n = getChildByTagName(n, link);
			if (n == null)
			{
				return null;
			}
			link = GSPath.getFirstLink(path);
			path = GSPath.removeFirstLink(path);
		}
		return n;
	}

	/**
	 * takes an xpath type expression of the form name/name/... and returns the
	 * first node that matches, or null if not found can include [i] indices.
	 * index numbers start at 0
	 */
	public static Node getNodeByPathIndexed(Node n, String path)
	{

		String link = GSPath.getFirstLink(path);
		int index = GSPath.getIndex(link);
		if (index != -1)
		{
			link = GSPath.removeIndex(link);
		}
		path = GSPath.removeFirstLink(path);
		while (!link.equals(""))
		{
			n = getChildByTagNameIndexed(n, link, index);
			if (n == null)
			{
				return null;
			}
			link = GSPath.getFirstLink(path);
			index = GSPath.getIndex(link);
			if (index != -1)
			{
				link = GSPath.removeIndex(link);
			}
			path = GSPath.removeFirstLink(path);
		}
		return n;
	}

	public static HashMap<String, Node> getChildrenMap(Node n)
	{

		HashMap<String, Node> map = new HashMap<String, Node>();
		Node child = n.getFirstChild();
		while (child != null)
		{
			String name = child.getNodeName();
			map.put(name, child);
			child = child.getNextSibling();
		}
		return map;
	}

	public static NodeList getChildrenByTagName(Node n, String name)
	{
		MyNodeList node_list = new MyNodeList();
		Node child = n.getFirstChild();
		while (child != null)
		{
			if (child.getNodeName().equals(name))
			{
				node_list.addNode(child);
			}
			child = child.getNextSibling();
		}
		return node_list;
	}

	public static NodeList getChildrenByTagNameNS(Node n, String namespace, String local_name)
	{
		MyNodeList node_list = new MyNodeList();
		Node child = n.getFirstChild();
		while (child != null)
		{
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNamespaceURI() != null && child.getNamespaceURI().equals(namespace) && child.getLocalName() != null && child.getLocalName().equals(local_name))
			{
				node_list.addNode(child);
			}
			child = child.getNextSibling();
		}
		return node_list;
	}

	/** Duplicates an element, but gives it a new name */
	public static Element duplicateWithNewName(Document owner, Element element, String element_name, boolean with_attributes)
	{
		return duplicateWithNewNameNS(owner, element, element_name, null, with_attributes);
	}

	/** Duplicates an element, but gives it a new name */
	public static Element duplicateWithNewNameNS(Document owner, Element element, String element_name, String namespace_uri, boolean with_attributes)
	{
		Element duplicate;
		if (namespace_uri == null)
		{
			duplicate = owner.createElement(element_name);
		}
		else
		{
			duplicate = owner.createElementNS(namespace_uri, element_name);
		}

		// Copy element attributes
		if (with_attributes)
		{
			NamedNodeMap attributes = element.getAttributes();
			for (int i = 0; i < attributes.getLength(); i++)
			{
				Node attribute = attributes.item(i);
				duplicate.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
			}
		}

		// Copy element children
		copyAllChildren(duplicate, element);
		
		return duplicate;
	}

	public static void copyAllChildren(Element to, Element from)
	{

		Document to_doc = to.getOwnerDocument();
		Node child = from.getFirstChild();
		while (child != null)
		{
			to.appendChild(to_doc.importNode(child, true));
			child = child.getNextSibling();
		}
	}

  public static void copyNode(Element to_elem, Node original_element) {
    to_elem.appendChild(to_elem.getOwnerDocument().importNode(original_element, true));

  }

   public static void copyNamedElement(Element to, Element from, String elem_name) {
      
      Document to_doc = to.getOwnerDocument();
      Node child = from.getFirstChild();
      while (child != null) {
        if (child.getNodeName().equals(elem_name)) {
          to.appendChild(to_doc.importNode(child, true));
          return;
        }
        child = child.getNextSibling();
      }
    }

  /* Have an element with a set of children, attributes name="x". Replace the specified item in the list with new_item. */
  /* this assumes we only have children of the same node name. if going tro have mixed lists, need to test on node name as well */
  public static void replaceNamedItemInList(Element item_list, String item_node_name, String name_att, String item_name, Element new_item) {
    Node child = item_list.getFirstChild();
    while(child != null) {
      if (child.getNodeName().equals(item_node_name) && ((Element)child).getAttribute(name_att).equals(item_name)) {
	//replace the element
	item_list.insertBefore(item_list.getOwnerDocument().importNode(new_item, true), child);
	item_list.removeChild(child);
	return;
      }
      child = child.getNextSibling();
    }

  }
 	/**
	 * Given a Node representing an Element or Document, will return the
	 * Element/docroot Element. Returns null if the Node was not an element.
	 */
	public static Element nodeToElement(Node node)
	{
		if (node == null)
		{
			return null;
		}
		short nodeType = node.getNodeType();

		if (nodeType == Node.DOCUMENT_NODE)
		{
			Document docNode = (Document) node;
			return docNode.getDocumentElement();
		}
		else if (nodeType == Node.ELEMENT_NODE)
		{
			return (Element) node;
		}
		else
		{
			String message = "Expecting Document or Element node type but got " + node.getNodeName() + "\nReturning null";
			System.err.println(message);
			logger.warn(message);
			return null;
		}
	}
 

    /** returns a basic request message */
    public static Element createBasicRequest(Document owner, String request_type, String to, UserContext userContext)
    {
	Element request = owner.createElement(REQUEST_ELEM);
	request.setAttribute(TO_ATT, to);
	request.setAttribute(TYPE_ATT, request_type);
	if (userContext == null) {
	    return request; // should we allow this??
	}
	request.setAttribute(LANG_ATT, userContext.getLanguage());
	Element userContextElem = owner.createElement("userContext");
	request.appendChild(userContextElem);
	userContextElem.setAttribute(LANG_ATT, userContext.getLanguage());
	userContextElem.setAttribute(USERNAME_ATT, userContext.getUsername());
	userContextElem.setAttribute(USER_ID_ATT, userContext.getUserID());
	userContextElem.setAttribute(GROUPS_ATT, userContext.getGroupsString());
	userContextElem.setAttribute(EDIT_ENABLED_ATT, userContext.getEditEnabled());
	
	return request;
    }

	public static Element createBasicResponse(Document owner, String from)
	{
		Element response = owner.createElement(RESPONSE_ELEM);
		response.setAttribute(FROM_ATT, from);
		response.setAttribute(TYPE_ATT, REQUEST_TYPE_PROCESS);
		return response;
	}

	public static Element createMetadataElement(Document owner, String name, String value)
	{
		Element metaElem = owner.createElement(METADATA_ELEM);
		metaElem.setAttribute(NAME_ATT, name);
		metaElem.setAttribute(VALUE_ATT, value);
		return metaElem;
	}

	public static Element createTextElement(Document owner, String elem_name, String text)
	{
		Element e = owner.createElement(elem_name);
		Text t = owner.createTextNode(text);
		e.appendChild(t);
		return e;

	}

	public static Element createTextElement(Document owner, String elem_name, String text, String att_name, String att_value)
	{
		Element e = owner.createElement(elem_name);
		e.setAttribute(att_name, att_value);
		Text t = owner.createTextNode(text);
		e.appendChild(t);
		return e;

	}

	public static Element createDisplayTextElement(Document owner, String text_name, String text)
	{
		Element e = owner.createElement(DISPLAY_TEXT_ELEM);
		e.setAttribute(NAME_ATT, text_name);
		Text t = owner.createTextNode(text);
		e.appendChild(t);
		return e;

	}

  // for parameters where value is an attribute
	public static Element createParameter(Document owner, String name, String value)
	{
		Element param = owner.createElement(PARAM_ELEM);
		param.setAttribute(NAME_ATT, name);
		param.setAttribute(VALUE_ATT, value);
		return param;
	}

  // for parameters where value is a text node
  public static Element createParameter2(Document owner, String name, String value) {
    Element param = owner.createElement(PARAM_ELEM);
    param.setAttribute(NAME_ATT, name);
    param.appendChild(owner.createTextNode(value));
    return param;
  }     

  public static void addParameterToList(Element param_list, String name, String value) {
    Element p = createParameter(param_list.getOwnerDocument(), name, value);
    param_list.appendChild(p);
  }
  public static void addParameter2ToList(Element param_list, String name, String value) {
    Element p = createParameter2(param_list.getOwnerDocument(), name, value);
    param_list.appendChild(p);
  }
	public static void addParametersToList(Element param_list, HashMap params)
	{
		if (params == null)
		{
			return;
		}

		Set items = params.entrySet();
		Iterator i = items.iterator();
		Document owner = param_list.getOwnerDocument();
		while (i.hasNext())
		{
			Map.Entry m = (Map.Entry) i.next();
			param_list.appendChild(createParameter(owner, (String) m.getKey(), (String) m.getValue()));
		}

	}

	public static Element createParameterDescription(Document owner, String id, String display_name, String type, String default_value, String[] option_ids, String[] option_names)
	{

		Element p = owner.createElement(PARAM_ELEM);
		p.setAttribute(NAME_ATT, id);
		p.setAttribute(TYPE_ATT, type);
		p.appendChild(createDisplayTextElement(owner, DISPLAY_TEXT_NAME, display_name));

		if (default_value != null)
		{
			p.setAttribute(DEFAULT_ATT, default_value);
		}
		if (option_ids != null && option_names != null)
		{
			for (int i = 0; i < option_ids.length; i++)
			{
				Element e = owner.createElement(PARAM_OPTION_ELEM);
				e.setAttribute(NAME_ATT, option_ids[i]);
				e.appendChild(createDisplayTextElement(owner, DISPLAY_TEXT_NAME, option_names[i]));
				p.appendChild(e);
			}
		}
		return p;
	}

	public static Element createParameterDescription2(Document owner, String id, String display_name, String type, String default_value, ArrayList<String> option_ids, ArrayList<String> option_names)
	{

		Element p = owner.createElement(PARAM_ELEM);
		p.setAttribute(NAME_ATT, id);
		p.setAttribute(TYPE_ATT, type);
		p.appendChild(createDisplayTextElement(owner, DISPLAY_TEXT_NAME, display_name));
		if (default_value != null)
		{
			p.setAttribute(DEFAULT_ATT, default_value);
		}
		if (option_ids != null && option_names != null)
		{
			for (int i = 0; i < option_ids.size(); i++)
			{
				Element e = owner.createElement(PARAM_OPTION_ELEM);
				e.setAttribute(NAME_ATT, option_ids.get(i));
				e.appendChild(createDisplayTextElement(owner, DISPLAY_TEXT_NAME, option_names.get(i)));
				p.appendChild(e);
			}
		}
		return p;
	}

	/** returns the element parent/node_name[@attribute_name='attribute_value'] 
	 if attribute_value is null, just returns the first one with that attribute */
	public static Element getNamedElement(Element parent, String node_name, String attribute_name, String attribute_value)
	{
	    if (parent == null) return null;
	    NodeList children = parent.getChildNodes();
		for (int i = 0; i < children.getLength(); i++)
		{
			Node child = children.item(i);
			if (child.getNodeName().equals(node_name))
			{
			  String att_val = ((Element) child).getAttribute(attribute_name);
			  if (!att_val.equals("") ) {
			    if (attribute_value == null || att_val.equals(attribute_value)) {
			      return (Element) child;
			    }
			  }
			}
		} // for each child

	    // not found
		return null;
	}

	public static Element getNamedElementNS(Element parent, String namespace_uri, String node_local_name, String attribute_name, String attribute_value)
	{
		NodeList children = parent.getChildNodes();
		for (int i = 0; i < children.getLength(); i++)
		{
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNamespaceURI() != null && child.getNamespaceURI().equals(namespace_uri) && child.getLocalName().equals(node_local_name))
			{
				if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
					return (Element) child;
			}
		}
		// not found
		return null;
	}

	public static NodeList getNamedElementsNS(Element parent, String namespace_uri, String node_local_name, String attribute_name, String attribute_value)
	{
		MyNodeList result = new MyNodeList();
		NodeList children = parent.getChildNodes();
		for (int i = 0; i < children.getLength(); i++)
		{
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNamespaceURI() != null && child.getNamespaceURI().equals(namespace_uri) && child.getLocalName().equals(node_local_name))
			{
				if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
					result.addNode(child);
			}
		}
		return result;
	}

  public static Element findMatchingElement(Element root, String node_local_name, String[] attribute_names, String[] attribute_values) {
    if (attribute_names.length == 0 || attribute_names.length != attribute_values.length)
    {
      return null;
    }
    NodeList elems = root.getElementsByTagName(node_local_name);
    for (int i = 0; i < elems.getLength(); i++)
    {
      Element current = (Element) elems.item(i);
      boolean nodeMatches = true;
      for (int j = 1; j < attribute_names.length; j++)
      {
        String currentName = attribute_names[j];
        String currentValue = attribute_values[j];
        if (!current.getAttribute(currentName).equals(currentValue))
        {
          nodeMatches = false;
          break;
        }
      }
      
      if (nodeMatches)
      {
        return current;
      }
    }
    return null;
  }

  // returns the child of parent that matches local_node - name, NS, attributes
  public static Element findEquivalentElement(Element parent, Element local_node) {
    Element local_clone = (Element)local_node.cloneNode(false);
    String node_name = local_clone.getNodeName();
    NodeList children = getChildrenByTagName(parent, node_name);
    for (int i=0; i<children.getLength(); i++) {
      Node child = children.item(i);
      Node clone = child.cloneNode(false);
      if (clone.isEqualNode(local_clone)) {
        return (Element)child;
      }
    }
    return null;
  }
  
	public static NodeList getElementsWithAttributesNS(Element parent, String namespace_uri, String node_local_name, String[] attribute_names, String[] attribute_values)
	{
		if (attribute_names.length == 0 || attribute_names.length != attribute_values.length)
		{
			return new MyNodeList();
		}

		MyNodeList result = new MyNodeList();

		NodeList matchingNodes = getNamedElementsNS(parent, namespace_uri, node_local_name, attribute_names[0], attribute_values[0]);
		for (int i = 0; i < matchingNodes.getLength(); i++)
		{
			Element current = (Element) matchingNodes.item(i);
			boolean nodeMatches = true;
			for (int j = 1; j < attribute_names.length; j++)
			{
				String currentName = attribute_names[j];
				String currentValue = attribute_values[j];
				if (!current.getAttribute(currentName).equals(currentValue))
				{
					nodeMatches = false;
					break;
				}
			}

			if (nodeMatches)
			{
				result.addNode(current);
			}
		}

		return result;
	}

	public static void removeElementsWithAttributesNS(Element parent, String namespace_uri, String node_local_name, String[] attribute_names, String[] attribute_values)
	{
		NodeList matchingNodes = getElementsWithAttributesNS(parent, namespace_uri, node_local_name, attribute_names, attribute_values);
		for (int i = 0; i < matchingNodes.getLength(); i++)
		{
			parent.removeChild(matchingNodes.item(i));
		}
	}

	// In element main, tries to find any previous occurrence of elements with xsl-template-name=templateName, 
	// and whose named attribute (attributeName) has the same value as the same attribute in node.
	// If this is the case, such a previous occurrence is removed from element main, since
	// the new node will contain a more specific redefinition of this element.
	public static void removeNamedElementNS(Element parent, String namespace_uri, String node_local_name, String attribute_name, String attribute_value)
	{
		if (attribute_value.equals(""))
		{
			// it has no identifying attribute, so we can't find any matches
			return;
		}

		Element old_elem = getNamedElementNS(parent, namespace_uri, node_local_name, attribute_name, attribute_value);
		if (old_elem != null)
		{
			parent.removeChild(old_elem);
		}
	}

	public static void removeNamedElementsNS(Element parent, String namespace, String node_local_name, String attribute_name, String attribute_value)
	{
		if (attribute_value.equals(""))
		{
			// it has no identifying attribute, so we can't find any matches
			return;
		}

		NodeList children = parent.getChildNodes();
		for (int i = children.getLength() - 1; i >= 0; i--)
		{
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNamespaceURI() != null && child.getNamespaceURI().equals(namespace) && child.getLocalName() != null && child.getLocalName().equals(node_local_name))
			{
				if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
					parent.removeChild(child);
			}
		}
	}

	/**
	 * returns a NodeList of elements:
	 * ancestor/node_name[@attribute_name='attribute_value']
	 */
	public static NodeList getNamedElements(Element ancestor, String node_name, String attribute_name, String attribute_value)
	{
		MyNodeList node_list = new MyNodeList();
		NodeList children = ancestor.getElementsByTagName(node_name);

		if (children != null && children.getLength() > 0)
		{

			for (int i = 0; i < children.getLength(); i++)
			{
				Node child = children.item(i);
				if (child.getNodeName().equals(node_name))
				{
					if (((Element) child).getAttribute(attribute_name).equals(attribute_value))
						node_list.addNode(child);
				}
			}
		}
		return node_list;
	}

	public static Element getLastElementByTagNameNS(Element main, String namespace, String node_name)
	{
		NodeList nodes = main.getElementsByTagNameNS(namespace, node_name);
		int len = nodes.getLength();
		if (len == 0)
		{
			return null;
		}
		return (Element) nodes.item(len - 1);
	}

	public static int SORT_TYPE_STRING = 0;
	public static int SORT_TYPE_INT = 1;
	public static int SORT_TYPE_FLOAT = 2;

	// sort type:
	public static Element insertIntoOrderedList(Element parent_node, String node_name, Element start_from_elem, Element new_elem, String sort_att, boolean descending)
	{
		if (new_elem == null)
			return null;
		Element cloned_elem = (Element) parent_node.getOwnerDocument().importNode(new_elem, true);
		if (start_from_elem == null)
		{
			parent_node.appendChild(cloned_elem);
			return cloned_elem;
		}

		Node current_node = start_from_elem;
		String insert_att = cloned_elem.getAttribute(sort_att);
		String list_att = start_from_elem.getAttribute(sort_att);
		while ((!descending && list_att.compareTo(insert_att) < 0) || (descending && list_att.compareTo(insert_att) > 0))
		{
			current_node = current_node.getNextSibling();
			if (current_node == null)
				break; // end of the list
			if (!current_node.getNodeName().equals(node_name))
			{
				continue; // not a valid node
			}
			list_att = ((Element) current_node).getAttribute(sort_att);
		}

		parent_node.insertBefore(cloned_elem, current_node);
		return cloned_elem;
	}

  public static String getMetadataValue(Element metadata_list, String name) {
    Element meta = getNamedElement(metadata_list, METADATA_ELEM, NAME_ATT, name);
    if (meta == null) {
      return "";
    }
    String att_value = meta.getAttribute(VALUE_ATT);
    if (att_value.equals("")) {
      // try the text
      att_value = getNodeText(meta);
    }
    return att_value;
  }

	// replaces < > " ' & in the original with their entities
	public static String xmlSafe(String original)
	{

		StringBuffer filtered = new StringBuffer(original.length());
		char c;
		for (int i = 0; i < original.length(); i++)
		{
			c = original.charAt(i);
			if (c == '>')
			{
				filtered.append("&gt;");
			}
			else if (c == '<')
			{
				filtered.append("&lt;");
			}
			else if (c == '"')
			{
				filtered.append("&quot;");
			}
			else if (c == '&')
			{
				filtered.append("&amp;");
			}
			else if (c == '\'')
			{
				filtered.append("&apos;");
			}
			else
			{
				filtered.append(c);
			}
		}
		return filtered.toString();
	}

	// replaces < > " ' & entities with their originals
	public static String unXmlSafe(String original)
	{

		StringBuffer filtered = new StringBuffer(original.length());
		char c;
		for (int i = 0; i < original.length(); i++)
		{
			c = original.charAt(i);
			if (c == '&')
			{
				int pos = original.indexOf(";", i);
				String entity = original.substring(i + 1, pos);
				if (entity.equals("gt"))
				{
					filtered.append(">");
				}
				else if (entity.equals("lt"))
				{
					filtered.append("<");
				}
				else if (entity.equals("apos"))
				{
					filtered.append("'");
				}
				else if (entity.equals("amp"))
				{
					filtered.append("&");
				}
				else if (entity.equals("quot"))
				{
					filtered.append("\"");
				}
				else
				{
					filtered.append("&" + entity + ";");
				}
				i = pos;
			}
			else
			{
				filtered.append(c);
			}
		}
		return filtered.toString();
	}

	public static void printXMLNode(Node e, boolean printText)
	{
		printXMLNode(e, 0, printText);
	}

	public static String xmlNodeToString(Node e)
	{
		StringBuffer sb = new StringBuffer("");
		xmlNodeToString(sb, e, true, "\t", 0);
		return sb.toString();
	}

	public static void xmlNodeToString(StringBuffer sb, Node e, boolean indent, String indentString, int depth)
	{
		if (e.getNodeType() == Node.TEXT_NODE)
		{
			if (e.getNodeValue() != "")
			{
				String text = e.getNodeValue();
				if (text != null)
				{
					text = text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("^[\\n\\r\\t\\s]*", "").replaceAll("[\\n\\r\\t\\s]*$", "");
					sb.append(text);
				}
			}
			return;
		}

		if (e.getNodeType() == Node.COMMENT_NODE)
		{
			if (e.getNodeValue() != "")
			{
				sb.append("<!--" + e.getNodeValue() + "-->");
			}
			return;
		}

		if (indent)
		{
			for (int i = 0; i < depth; i++)
			{
				sb.append(indentString);
			}
		}

		sb.append('<');
		sb.append(e.getNodeName());
		NamedNodeMap attrs = e.getAttributes();
		if (attrs != null)
		{
			for (int i = 0; i < attrs.getLength(); i++)
			{
				Node attr = attrs.item(i);
				sb.append(' ');
				sb.append(attr.getNodeName());
				sb.append("=\"");
				sb.append(attr.getNodeValue().replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"));
				sb.append('"');
			}
		}

		if (e.hasChildNodes())
		{
			boolean hasElements = false;
			boolean indentSwapped = false;

			Node child = e.getFirstChild();
			do
			{
				if (child.getNodeType() == Node.ELEMENT_NODE)
				{
					hasElements = true;
				}
				if (child.getNodeType() == Node.TEXT_NODE && indent)
				{
					if (child.getNodeValue() != null && child.getNodeValue().trim().length() > 0)
					{
						indentSwapped = true;
						indent = false;
					}
				}
			} while ((child = child.getNextSibling()) != null);

			sb.append(">");
			if (hasElements && indent)
			{
				sb.append("\n");
			}

			child = e.getFirstChild();
			do
			{
				xmlNodeToString(sb, child, indent, indentString, depth + 1);
			} while ((child = child.getNextSibling()) != null);

			if (indent)
			{
				for (int i = 0; i < depth; i++)
				{
					sb.append(indentString);
				}
			}

			sb.append("</" + e.getNodeName() + ">");

			if ((hasElements && indent) || indentSwapped)
			{
				sb.append("\n");
			}
		}
		else
		{
			sb.append("/>");

			if (indent)
			{
				sb.append("\n");
			}
		}

		/*
		boolean hasElements = false;
		boolean indentSwapped = false;
		for (int i = 0; i < children.getLength(); i++)
		{
			if (children.item(i).getNodeType() == Node.ELEMENT_NODE)
			{
				hasElements = true;
			}
			if (children.item(i).getNodeType() == Node.TEXT_NODE && indent)
			{
				if (children.item(i).getNodeValue() != null && children.item(i).getNodeValue().trim().length() > 0)
				{
					indentSwapped = true;
					indent = false;
				}
			}
		}

		if (children == null || children.getLength() == 0)
		{
			sb.append("/>");

			if (indent)
			{
				sb.append("\n");
			}
		}
		else
		{
			sb.append(">");
			if (hasElements && indent)
			{
				sb.append("\n");
			}

			int len = children.getLength();
			for (int i = 0; i < len; i++)
			{
				xmlNodeToString(sb, children.item(i), indent, indentString, depth + 1);
			}

			if (indent)
			{
				for (int i = 0; i < depth; i++)
				{
					sb.append(indentString);
				}
			}

			sb.append("</" + e.getNodeName() + ">");

			if ((hasElements && indent) || indentSwapped)
			{
				sb.append("\n");
			}
		}
		*/
	}

	public static void printXMLNode(Node e, int depth, boolean printText)
	{ //recursive method call using DOM API...

		if (e == null)
		{
			return;
		}

		for (int i = 0; i < depth; i++)
			System.out.print(' ');

		if (e.getNodeType() == Node.TEXT_NODE)
		{
			if (printText)
			{
				System.out.println(e.getNodeValue());
			}
			else
			{
				System.out.println("text");
			}
			return;
		}

		System.out.print('<');
		System.out.print(e.getNodeName());
		NamedNodeMap attrs = e.getAttributes();

		if (attrs != null)
		{
			for (int i = 0; i < attrs.getLength(); i++)
			{
				Node attr = attrs.item(i);
				System.out.print(' ');
				System.out.print(attr.getNodeName());
				System.out.print("=\"");
				System.out.print(attr.getNodeValue());
				System.out.print('"');
			}
		}

		NodeList children = e.getChildNodes();

		if (children == null || children.getLength() == 0)
			System.out.println("/>");
		else
		{

			System.out.println('>');

			int len = children.getLength();
			for (int i = 0; i < len; i++)
			{
				printXMLNode(children.item(i), depth + 1, printText);
			}

			for (int i = 0; i < depth; i++)
				System.out.print(' ');

			System.out.println("</" + e.getNodeName() + ">");
		}
	}

	private static void elementToLogAsString(String prefix, Element e, boolean indent, boolean debugEncoding)
	{
		String str = prefix + "\n" + elementToString(e, indent, debugEncoding);
		System.err.println(str);
		logger.info(str);
	}
	
	// hex/unicode codepoint used only for those chars that are beyond printable/basic ASCII
	public static void elementToLogAsUnicodeDebugString(String prefix, Element e, boolean indent)
	{
		elementToLogAsString(prefix, e, indent, true);
	}
	
	public static void elementToLogAsString(String prefix, Element e, boolean indent)
	{
		elementToLogAsString(prefix, e, indent, false);
	}

	// pass in debugEncoding=true to investigate encoding issues. This function will then return non-basic ASCII characters in hex
	private static String elementToString(Element e, boolean indent, boolean debugEncoding)
	{
		String str = "";
		try
		{
			TransformerFactory tf = TransformerFactory.newInstance();
			Transformer trans = tf.newTransformer();
			StringWriter sw = new StringWriter();
			if (indent)
			{
				trans.setOutputProperty(OutputKeys.INDENT, "yes");
			}
			else
			{
				trans.setOutputProperty(OutputKeys.INDENT, "no");
			}
			trans.transform(new DOMSource(e), new StreamResult(sw));
			str += sw.toString();
			
			// if debugging encoding issues, then encode unicode code pts as hex for all but non-alphanumeric and space/tab/newline chars
			if(debugEncoding) str = Misc.debugUnicodeString(str);
		}
		catch (Exception ex)
		{
			str += "Exception: couldn't write " + e + " to log";
		}
		finally
		{
			return str;
		}
	}

	public static String elementToString(Element e, boolean indent)
	{
		return elementToString(e, indent, false);
	}
	
	// hex/unicode codepoint used only for those chars that are beyond printable/basic ASCII
	public static String elementToUnicodeDebugString(Element e, boolean indent)
	{
		return elementToString(e, indent, true);
	}
	
	public static ArrayList<String> getGroupsFromSecurityResponse(Element securityResponse)
	{
		ArrayList<String> groups = new ArrayList<String>();

		Element groupList = (Element) getChildByTagName(securityResponse, GROUP_ELEM + LIST_MODIFIER);
		if (groupList == null)
		{
			return groups;
		}

		NodeList groupElems = getChildrenByTagName(groupList, GROUP_ELEM);

		for (int i = 0; i < groupElems.getLength(); i++)
		{
			Element groupElem = (Element) groupElems.item(i);
			// an empty attribute will return "". some code depends on having "" for groups, so leave it in for now.
			groups.add(groupElem.getAttribute(NAME_ATT));
		}

		return groups;
	}

    
	/**
	 * this looks for any classifier specific display or format info from
	 * extra_info and adds it in to the correct place in info.
	 *
	 * This was moved here from AbstractBrowse.java where this was a static method
	 * because FedoraServiceProxy.java (which is both a Browse and Query Service) needs
	 * to use the entire method and thus was forced to use it in a static manner.
	 */
	public static boolean extractExtraClassifierInfo(Element info, Element extra_info)
	{
		if (extra_info == null)
		{
			return false;
		}

		Document owner = info.getOwnerDocument();
		// so far we have display and format elements that we need for classifiers
		NodeList classifiers = info.getElementsByTagName(GSXML.CLASSIFIER_ELEM);
		Element config_browse = (Element) GSXML.getChildByTagName(extra_info, GSXML.BROWSE_ELEM);

		for (int i = 0; i < classifiers.getLength(); i++)
		{
			Element cl = (Element) classifiers.item(i);
			String name = cl.getAttribute(GSXML.NAME_ATT);

			//Element node_extra = GSXML.getNamedElement(config_browse,
			//					       GSXML.CLASSIFIER_ELEM,
			//					       GSXML.NAME_ATT,
			//					       name);
			//now use the position to get the node - CL1 
			// assumes the same number of classifiers in collectionCOnfig as in buildConfig
			//            int position = Integer.parseInt(name.substring(2));

			Element node_extra = null;
			NodeList cls = config_browse.getElementsByTagName(GSXML.CLASSIFIER_ELEM);
			//if (position >0 && position <= cls.getLength()) {
			//    node_extra  = (Element) cls.item((position -1)); 
			//}
			if (i < cls.getLength())
			{
				node_extra = (Element) cls.item(i);
			}

			if (node_extra == null)
			{
				logger.error("GS2REtrieve: haven't found extra info for classifier named " + name);
				continue;
			}

                        String hidden = node_extra.getAttribute(GSXML.HIDDEN_ATT);
                        if (!hidden.equals("")) {
                          cl.setAttribute(GSXML.HIDDEN_ATT, hidden);
                          
                        }
			// get the display elements if any - displayName
			NodeList display_names = node_extra.getElementsByTagName(GSXML.DISPLAY_TEXT_ELEM);
			if (display_names != null)
			{
				Element display = owner.createElement(GSXML.DISPLAY_ELEM);
				for (int j = 0; j < display_names.getLength(); j++)
				{
					Element e = (Element) display_names.item(j);
					cl.appendChild(owner.importNode(e, true));
				}
			}

			// get the format element if any 
			Element format = (Element) GSXML.getChildByTagName(node_extra, GSXML.FORMAT_ELEM);
			if (format != null)
			{ // append to index info
				cl.appendChild(owner.importNode(format, true));
			}
			    
		} // for each classifier

		// now check for default format info
		Element default_format = (Element) GSXML.getChildByTagName(config_browse, GSXML.FORMAT_ELEM);
		if (default_format != null)
		{ // append to  info
			info.appendChild(owner.importNode(default_format, true));
		}

		return true;
	}
}
