All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.imsglobal.lti.XMLMap Maven / Gradle / Ivy

Go to download

BasicLTI Utilities are a set of utility classes to aid in the development of BasicLTI consumers and providers. They deal with much of the heavy lifting and make the process more opaque to the developer.

The newest version!
/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/lti/trunk/lti-util/src/java/org/imsglobal/lti/XMLMap.java $
 * $Id: XMLMap.java 308965 2014-04-29 06:37:09Z [email protected] $
 **********************************************************************************
 *
 * Copyright (c) 2009 IMS GLobal Learning Consortium, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License. 
 *
 **********************************************************************************/

package org.imsglobal.lti;

/* 
 * This is a little project I call mdom.org which stands for "Map-Dom" or "XML Doms in Maps" 
 * or "XML Documents meet Java Maps" - clearly there is homage to XPath style parsing in the
 * formation of the keys in the Maps - but XPath is not used.
 * 
 * It is my attempt to build a simple, self-contained, static class to make XML parsing 
 * REALLY simple in Java - the idea is to approximate the ease of taking apart and 
 * putting together chunks of XML in languages like Perl, Python, PHP, and Ruby.  
 * While the typed nature of Java makes it so there is a little extra syntax, it has 
 * been reduced to some pretty simple stuff with really forgiving calls.
 * 
 * This has had several names - initially it is called XMLMap - these are pre-release
 * versions ad I leave copies of this around as I move through projects and use 
 * the software.  When I get serious, I will distribute this as org.mdom.MDom and 
 * distribute it as a jar with versions and all that.
 * 
 * It is really easy to take apart XML as long as there are no repeated elements.
 * The function getMap returns a map of keys and values - the keys are the path 
 * starting at the root node of the XML and the value is the element stored inside.
 * You can get attributes as well.  This example:
 * 
 *  
 *	  B
 *	  
 *	    D
 *	  
 *  
 * 
 * Map xmlMap = XMLMap.getMap("BD");
 * 
 * Ends up with the following in the map:
 * 
 *   /a/b!x = X
 *   /a/b = B
 *   /a/c/d = D
 *   
 * You simply use the hash get method to pull out the information.
 * 
 *   System.out.println(xmlMap.get("/a/b"))
 * 
 * Once it is parsed into the Map - everything is quick and simple.
 * 
 * Things are similar in creating XML - You make a TreeMap and put entries in using put()
 * 
 *   Map newMap = new TreeMap;
 *   newMap.put("/a/b!x","X");
 *   newMap.put("/a/b", "B");
 *   newMap.put("/a/b/c", "C");
 *   String newXml = XMLMap.getXML(simpleMap, true);
 *
 * Another technique is the concept of submaps - you can extract a submap from a Map and then 
 * graft it directly onto some other bit of XML.
 * 
 *  Map subMap = XMLMap.selectSubMap(tm, "/a/c");
 *	Map joinedMap = new TreeMap();
 *	System.out.println("subMap="+subMap);
 *	joinedMap.put("/top/id", "1234");
 *	joinedMap.put("/top/fun", subMap); // Graft the map onto this node
 *	String joinedXml = XMLMap.getXML(joinedMap, true);
 *	System.out.println("joinedXML\n"+joinedXml);
 *
 * Produces this XML:
 * 
 *   
 *     
 *       D
 *     
 *     1234
 *   
 * 
 * The portion "below" /a/c was extracted and grafted onto the new XML at /top/fun.
 * You can mix strings and Maps in the same map and you can have maps within maps.
 * Once you switch to the Map you can even add an array of strings to an entry.
 * 
 *  Map arrayMap = new TreeMap();
 *	String [] strar = { "first", "second", "third" };
 *  arrayMap.put("/root/stuff", strar);
 *  String arrayXml = XMLMap.getXML(arrayMap, true);
 *  
 *  Produces:
 *  
 *  
 *    first
 *    second
 *    third
 *  
 *
 * The other major concept is how we parse XML and handle multiple items - such as in an RSS feed.
 * When faced with a string of XML where you expect to get sets of items you need to parse the XML
 * and get a "full" map - in this case, when the XMLMap parser sees multiple peer child nodes it
 * returns a List> in the entry. This makes getting lists of Maps realy easy 
 * but can make the basic looking things up in the map a little harder.  There are two approaches to 
 * this - you can either flatten the map or use the getString method to pull out all of the strings.
 * The getString method does not "go into" any lists of maps -  flattening does flatten through 
 * lists of maps, picking the first element of each list.
 * 
 * Here is a way to look up single elements in a Full Map :
 * 
 *   Map rssFullMap = XMLMap.getFullMap(rssText);
 *   System.out.println("Rss Version="+XMLMap.getString(rssFullMap,"/rss!version"));
 *   System.out.println("Chan-title="+XMLMap.getString(rssFullMap,"/rss/channel/title"));
 *
 * Here is how you flatten the Map int a Map and use get to lookup
 *     
 *   Map rssStringMap = XMLMap.flattenMap(rssFullMap);
 *   System.out.println("Rss Version="+rssStringMap.get("/rss!version"));
 *   System.out.println("Chan-title="+rssStringMap.get("/rss/channel/title"));
 *   
 * Iterating through a Full Map is pretty easy:

 *   for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) {
 *      System.out.println("=== Item ===");
 *      System.out.println(" Item-title="+XMLMap.getString(rssItem, "/title"));
 *   }
 *   
 * If you have nested sets of elements - you will get back a List> that can 
 * also be iterated.  In this example, we get a list of sites, then each site has a list of tools
 * and each tool has a list of properties.  The getList() method returns empty lists so 
 * that this code works even if the elements are not present or empty - the loops simply 
 * iterate zero times:
 *   
 *   Map theMap = XMLMap.getFullMap(bob);
 *   List> theList = XMLMap.getList(theMap, "/sites/site");
 *   for ( Map siteMap : theList) {
 *     System.out.println("Id="+XMLMap.getString(siteMap,"/id"));
 *     for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) {
 *        System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid"));
 *        for ( Map property : XMLMap.getList(toolMap, "/properties/property")) {
 *       	System.out.println("key="+XMLMap.getString(property, "/key"));
 *        	System.out.println("val="+XMLMap.getString(property, "/val"));
 *        }
 *      }
 *    }
 * 
 * You can retrieve an element within a site using getString() and then iterate through the 
 * sub-elements using getList().
 * 
 * There is a convenient variation of the getList() method which takes a String which combines 
 * the making of the map and retrieving of the list is you have no other use for the map:
 * 
 *   for ( Map siteMap : XMLMap.getList(bob,"/sites/site")) {
 *     System.out.println("Id="+XMLMap.getString(siteMap,"/id"));
 *     ...
 *    }
 *   
 * This class has static unit tests built in and a static main that can run the sample code and produce
 * output.  This is to insure that the jar file is 100% Self-contained.
 * 
 * TO DO:
 * 
 */

import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.TreeMap;
import java.util.Iterator;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;

/**
 * a simple utility class for REST style XML
 * kind of lets us act like we are in PHP.
 */
public class XMLMap {

	private static boolean DF = false;
	
	public static Map getMap(String str)
	{
		if ( str == null ) return null;
		Document doc = documentFromString(str);
		if ( doc == null ) return null;
		return getMap(doc);
	}

	public static Map getMap(Node doc)
	{
		Map tm =  getObjectMap(doc, false);
		if ( tm == null ) return null;
		return flattenMap(tm);
	}
	
	public static Map flattenMap(Map theMap)
	{
		if ( theMap == null ) return null;
		// Reduce to the first column of elements for the simple return value
		TreeMap retval = new TreeMap ();
		Iterator iter = theMap.keySet().iterator();
		while( iter.hasNext() ) {
			String key = iter.next();
			Object value = theMap.get(key);
			// No need to handle String[] - because they will not
			// be stored when doFull == false
			if ( value instanceof String ) {
				String svalue = (String) value;
				// doDebug(d,key+" = " + value);
				if ( value != null ) retval.put(key,svalue);
			}
		}
		return retval;
	}

	public static Map getFullMap(Node doc)
	{
		return getObjectMap(doc, true);
	}

	public static Map getFullMap(String str)
	{
		if ( str == null ) return null;
		Document doc = documentFromString(str);
		if ( doc == null ) return null;
		return getObjectMap(doc, true);
	}

	private static Map getObjectMap(Node doc, boolean doFull)
	{
		if ( doc == null ) return null;
		Map tm = new TreeMap();
		recurse(tm, "", doc, doFull,0);
		return tm;
	}

	// A Utility Method we expose so folks can reuse if they like
	public static Document documentFromString(String input)
	{
		try{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 
			factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 
			factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 
			DocumentBuilder parser = factory.newDocumentBuilder();
			Document document = parser.parse(new ByteArrayInputStream(input.getBytes()));
			return document;
		} catch (Exception e) {
			return null;
		}
	}

	private static String addSlash(String path)
	{
		if ( path == null ) return "/";
		if ( path.trim().equals("/") ) return "/";
		return path + "/";
	}
	
	@SuppressWarnings({ "unused", "static-access" })
	private static void recurse(Map tm, String path, Node parentNode, boolean doFull, int d) 
	{
		if ( DF ) doDebug(d,"> recurse path="+path+" parentNode="+ nodeToString(parentNode));
		d++;

		NodeList nl = parentNode.getChildNodes();
		NamedNodeMap nm = parentNode.getAttributes();

		// Count the TextNodes
		int nodeCount = 0;
		String value = null;
		
		// Insert the text node if we find one
		if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) {
			Node node = nl.item(i);
			if (node.getNodeType() == node.TEXT_NODE) {
				value = node.getNodeValue();
				if ( value == null ) break;
				if ( value.trim().length() < 1 ) break;
				// doDebug(d,"Adding path="+path+" value="+node.getNodeValue());
				tm.put(path,node.getNodeValue());
				break;  // Only the first one
			}
		}
		
		// Now loop through and add the attribute values 
		if ( nm != null ) for (int i = 0; i< nm.getLength(); i++ ) {
			Node node = nm.item(i);
			if (node.getNodeType() == node.ATTRIBUTE_NODE) {
				String name = node.getNodeName();
				value = node.getNodeValue();
				// doDebug(d,"ATTR "+path+"("+name+") = "+node.getNodeValue());
				if ( name == null || name.trim().length() < 1 || 
						value == null || value.trim().length() < 1 ) continue;  

				String newPath = path+"!"+name;
				tm.put(newPath,value);
			}
		}
		
		// If we are not doing the full DOM - we only traverse the first child
		// with the same name - so we use a set to record which nodes 
		// we have gone down.
		if ( ! doFull ) {
			// Now descend the tree to the next level deeper !!
			Set  done = new HashSet();
			if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) {
				Node node = nl.item(i);
				if (node.getNodeType() == node.ELEMENT_NODE && ( ! done.contains(node.getNodeName())) ) {
					if ( DF ) doDebug(d,"Going down the rabbit hole path="+path+" node="+node.getNodeName());
					recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d);
					if ( DF ) doDebug(d,"Back from the rabbit hole path="+path+" node="+node.getNodeName());
					done.add(node.getNodeName());	
				}
			}
			d--;
			if ( DF ) doDebug(d,"< recurse path="+path+" parentNode="+ nodeToString(parentNode));
			return;
		}

		// If we are going to do the full expansion - we need to know when 
		// There are more than one child with the same name.  If there are more
		// One child, we make list of Maps.

		Map childMap = new TreeMap();
		if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) {
			Node node = nl.item(i);
			if (node.getNodeType() == node.ELEMENT_NODE ) {
				Integer count = childMap.get(node.getNodeName());
				if ( count == null ) count = new Integer(0);
				count = count + 1;
				// Insert or Replace
				childMap.put(node.getNodeName(), count);
			}
		}
		
		if ( childMap.size() < 1 ) return;
		
		// Now go through the children nodes and make a List of Maps
		Iterator iter = childMap.keySet().iterator();
		Map>> nodeMap = new TreeMap>>();
		while ( iter.hasNext() ) {
			String nextChild = iter.next();
			if ( nextChild == null ) continue;
			Integer count = childMap.get(nextChild);
			if ( count == null ) continue;
			if ( count < 2 ) continue;
			if ( DF ) doDebug(d,"Making a List for "+nextChild);
			List> newList = new ArrayList>();
			nodeMap.put(nextChild,newList);
		}
		
		// Now descend the tree to the next level deeper !!
		if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) {
			Node node = nl.item(i);
			if (node.getNodeType() == node.ELEMENT_NODE ) {
				String childName = node.getNodeName();
				if ( childName == null ) continue;
				List> mapList = nodeMap.get(childName);
				if ( mapList == null ) {
					if ( DF ) doDebug(d,"Going down the single rabbit hole path="+path+" node="+node.getNodeName());
					recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d);
					if ( DF ) doDebug(d,"Back from the single rabbit hole path="+path+" node="+node.getNodeName());
				} else {
					if ( DF ) doDebug(d,"Going down the multi rabbit hole path="+path+" node="+node.getNodeName());
					Map newMap = new TreeMap();
					recurse(newMap,"/",node,doFull,d);
					if ( DF ) doDebug(d,"Back from the multi rabbit hole path="+path+" node="+node.getNodeName()+" map="+newMap);
					if ( newMap.size() > 0 ) mapList.add(newMap);
				}
			}
		}
		
		// Now append the multi-node maps to our current map
		Iterator iter2 = nodeMap.keySet().iterator();
		while ( iter2.hasNext() ) {
			String nextChild = iter2.next();
			if ( nextChild == null ) continue;
			List> newList = nodeMap.get(nextChild);
			if ( newList == null ) continue;
			if ( newList.size() < 1 ) continue;
			if ( DF ) doDebug(d,"Adding sub-map name="+nextChild+" list="+newList);
			tm.put(path+"/"+nextChild, newList);
		}
		d--;
        if ( DF ) doDebug(d,"< recurse path="+path+" parentNode="+ nodeToString(parentNode));
	}

	public static String getXML(Map tm)
	{
		Document document = getXMLDom(tm);
		if ( document == null ) return null;
		return documentToString(document, false);
	}

	public static String getXMLFragment(Map tm, boolean pretty)
	{
		String retval = getXML(tm, pretty);
		if ( retval.startsWith(" 0 ) retval = retval.substring(pos);
		}
		return retval;
	}

	public static String getXML(Map tm, boolean pretty)
	{
		Document document = getXMLDom(tm);
		if ( document == null ) return null;
		String retval = documentToString(document, pretty);
		// Since the built in transform seems unable to indent
		// We patch it ourselves to keep from being ugly
		if ( pretty ) {
			retval = prettyPostProcess(retval);
		}
		return retval;
	}
	
	// This process a pretty print from an input string - 
	// It does it the hard way - using the methods in this class.
	// It may not be the ideal way to pretty print a XML String
	// but it is our way and we want to be D.R.Y. here...  
	// As such you may see some error messages from 
	// the XMLMap class in the pretty printing.
	public static String prettyPrint(String input)
	{
		Map theMap = XMLMap.getFullMap(input);
        return XMLMap.getXML(theMap, true);
	}

	private static String prettyPostProcess(String inString)
	{
		StringBuffer sb = new StringBuffer();
		int depth = 0;
		boolean newLine = false;
		for (int i=0; i tm)
	{
		if ( tm == null ) return null;
		Document document = null;

		try{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 
			factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 
			factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 
			DocumentBuilder parser = factory.newDocumentBuilder();
			document = parser.newDocument();
		} catch (Exception e) {
			return null;
		}

		iterateMap(document, document.getDocumentElement(), tm, 0);
		return document;
	}

	/*  Remember that the map is a linear list of entries
    /a/b B1
    /a/c Map
         /x X1
         /y Y1
         /y!r R1
    /a/c!q Q1
    /a/d D1

    
      B1
      
         X1
         Y1
      
      D1
    
	 */
	private static void iterateMap(Document document, Node parentNode, Map tm, int d)
	{
		if ( DF ) doDebug(d,"> IterateMap parentNode= "+ nodeToString(parentNode));
		d++;
		Iterator iter = tm.keySet().iterator();
		while( iter.hasNext() ) {
			String key = (String) iter.next();
			if ( key == null ) continue;
			if ( ! key.startsWith("/") ) continue;  // Skip
			Object obj = tm.get(key);
			if ( obj == null ) {
				continue;
			} if ( obj instanceof String ) {
				storeInDom(document, parentNode, key, (String) obj, 0, d);
			} else if ( obj instanceof String [] ) {
				String [] strArray = (String []) obj;
				if ( DF ) doDebug(d,"Looping through an array of length "+strArray.length);
				for(int i=0; i < strArray.length; i++ ) {
					storeInDom(document, parentNode, key, strArray[i], i, d);
				} 
			} else if ( obj instanceof Map ) {
				Map subMap = (Map) obj;
				Node startNode = getNodeAtPath(document, parentNode, key, 0, d);
				if ( DF ) doDebug(d,"descending into Map path="+key+" startNode="+ nodeToString(startNode));
				iterateMap(document, startNode, subMap, d);
				if ( DF ) doDebug(d,"back from descent Map path="+key+" startNode="+ nodeToString(startNode));
			} else if ( obj instanceof List ) {
				List lst = (List) obj;
				if ( DF ) doDebug(d,"Have a list that is this long "+lst.size());
				Iterator listIter = lst.iterator();
				int newPos = 0;
				while ( listIter.hasNext() ) {
					Object listObj = listIter.next();
					if ( DF ) doDebug(d,"Processing List element@"+newPos+" "+listObj.getClass().getName());
					if ( listObj instanceof String ) {
						storeInDom(document, parentNode, key, (String) listObj, newPos, d);
						newPos++;
					} if ( listObj instanceof Map ) {
						Map subMap = (Map) listObj;
						if ( DF ) doDebug(d,"Retrieving key from  List-Map path="+key+"@"+newPos);
						Node startNode = getNodeAtPath(document, parentNode, key, newPos, d);
						if ( DF ) doDebug(d,"descending into List-Map path="+key+"@"+newPos+" startNode="+ nodeToString(startNode));
						iterateMap(document, startNode, subMap, d);
						if ( DF ) doDebug(d,"back from descent List-Map path="+key+"@"+newPos+" startNode="+ nodeToString(startNode));
						newPos++;
					} else {
						System.out.println("XMLMap Encountered an object of type "+obj.getClass().getName()+" in a List which should contain only Map objects");
					}
				}
 			} else {
				if ( DF ) doDebug(d,"Found a "+obj.getClass().getName()+" do not know how to iterate.");
			}
		}
		d--;
		if ( DF ) doDebug(d,"< IterateMap parentNode = "+ nodeToString(parentNode));
	}

	private static void storeInDom(Document document, Node parentNode, String key, String value, int nodePos, int d)
	{
		if ( DF ) doDebug(d,"> storeInDom"+key+"@"+ nodePos + " = " + value + " parent="+ nodeToString(parentNode));
		d++;
		if ( document == null || key == null || value == null ) return;
		if ( parentNode == null ) parentNode = document;
		if ( DF ) doDebug(d,"parentNode I="+ nodeToString(parentNode));

		String [] newPath = key.split("/");
		if ( DF ) doDebug(d,"newPath = "+outStringArray(newPath));
		String nodeAttr = null;
		for ( int i=1; i< newPath.length; i++ )
		{
			String nodeName = newPath[i];
			if ( i == newPath.length-1 ) {
				// doDebug(d,"Splitting !="+nodeName);
				// check to see if we have a nodename=attributename
				String [] nodeSplit = nodeName.split("!");
				if ( nodeSplit.length > 1 ) {
					nodeName = nodeSplit[0];
					nodeAttr = nodeSplit[1];
					// doDebug(d,"new nodeName="+nodeName+" nodeAttr="+nodeAttr);
				}
				parentNode = getOrAddChildNode(document, parentNode, nodeName, nodePos, d);
			} else {
				parentNode = getOrAddChildNode(document, parentNode, nodeName, 0, d);
			}
		}
		// doDebug(d,"parentNode after="+ nodeToString(parentNode));

		if ( nodeAttr != null )
		{
			if ( value!= null && parentNode instanceof Element ) 
			{
				Element element = (Element) parentNode;
				// doDebug(d,"Adding an attribute "+nodeAttr);
				element.setAttribute(nodeAttr,value);
			}
		}
		else if ( value != null ) 
		{
			Text newNode = document.createTextNode(value);
			parentNode.appendChild(newNode);
		}
		d--;
		// doDebug(d,"xml="+documentToString(document,false));
		// doDebug(d,"< storeInDom"+key+" = " + value);
	}

	// Note - sadly this does not "return" the attr name - hence we need 
	// to replicate this code in storeInDom :(
	private static Node getNodeAtPath(Document document, Node parentNode, String path, int nodePos, int d)
	{
		if ( parentNode == null ) parentNode = document;
		if ( DF ) doDebug(d,"> getNodeAtPath path@" + nodePos + "="+path+" parentNode="+ nodeToString(parentNode));
		d++;

		String [] newPath = path.split("/");
		// doDebug(d,"newPath = "+outStringArray(newPath));
		for ( int i=1; i< newPath.length; i++ )
		{
			String nodeName = newPath[i];
			if ( i == newPath.length-1 ) {
				// doDebug(d,"Splitting !="+nodeName);
				// check to see if we have a nodename=attributename
				String [] nodeSplit = nodeName.split("!");
				if ( nodeSplit.length > 1 ) {
					nodeName = nodeSplit[0];
					// doDebug(d,"new nodeName="+nodeName);
				}
				parentNode = getOrAddChildNode(document, parentNode, nodeName, nodePos, d);
			} else {
				parentNode = getOrAddChildNode(document, parentNode, nodeName, 0, d);
			}	
		}
		d--;
		if ( DF ) doDebug(d,"< getNodeAtPath returning="+ nodeToString(parentNode));
		return parentNode;
	}

	@SuppressWarnings("static-access")
	private static Node getOrAddChildNode(Document doc, Node parentNode, String nodeName,int whichNode, int d)
	{
		if ( DF ) doDebug(d,"> getOrAddChildNode name="+nodeName+"@"+whichNode+" parentNode="+ nodeToString(parentNode));
		d++;
		if ( nodeName == null || parentNode == null) return null;

		// Check to see if we are somewhere in an index
		int begpos = nodeName.indexOf('[');
		int endpos = nodeName.indexOf(']');
		// doDebug(d,"Looking for bracket ipos="+begpos+" endpos="+endpos);
		if ( begpos > 0 && endpos > begpos && endpos < nodeName.length() ) {
			String indStr = nodeName.substring(begpos+1,endpos);
			if ( DF ) doDebug(d,"Index String = "+ indStr);
			nodeName = nodeName.substring(0,begpos);
			if ( DF ) doDebug(d,"New Nodename="+nodeName);
			Integer iVal = new Integer(indStr); 
			if ( DF ) doDebug(d,"Integer = "+iVal);
			whichNode = iVal;
		}
		
		NodeList nl = parentNode.getChildNodes();
		int foundNodes = -1;
		if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) {
			Node node = nl.item(i);
			// doDebug(d,"length= " +nl.getLength()+ " i="+i+" NT="+node.getNodeType());
			// doDebug(d,"searching nn="+nodeName+" nc="+node.getNodeName());
			if (node.getNodeType() == node.ELEMENT_NODE) {
				if ( nodeName.equals(node.getNodeName()) ) {
					foundNodes++;
					d--;
					if ( DF ) doDebug(d,"< getOrAddChildNode found name="+ nodeToString(node));
					if ( DF ) doDebug(d,"foundNodes = "+foundNodes+" looking for node="+whichNode);
					if ( foundNodes >= whichNode ) return node;
				}
			}
		}

		Element newNode = null;
		while ( foundNodes < whichNode ) {
			foundNodes++;
			if ( DF ) doDebug(d,"Adding node at position " + foundNodes + " moving toward " + whichNode);
			if ( nodeName == null ) continue;
			newNode = doc.createElement(nodeName);
			if ( DF ) doDebug(d,"Adding "+nodeName+" at "+ nodeToString(parentNode)+" in "+doc);
			parentNode.appendChild(newNode);
			if ( DF ) doDebug(d,"xml="+documentToString(doc,false));
			if ( DF ) doDebug(d,"getOrAddChildNode added newnode="+ nodeToString(newNode));
		}
		d--;
		if ( DF ) doDebug(d,"< getOrAddChildNode added newnode="+ nodeToString(newNode));
		return newNode;
	}

	public static String outStringArray(String [] arr)
	{
		if ( arr == null ) return null;
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < arr.length; i++ ) {
			if ( i > 0 ) sb.append(" ");
			sb.append("["+i+"]=");
			sb.append(arr[i]);
		}
		return sb.toString();
	}

	public static String nodeToString(Node node)
	{
		if ( node == null ) return null;
		String retval = node.getNodeName();
		while ( (node = node.getParentNode()) != null ) {
			retval = node.getNodeName() + "/" + retval;
		}
		return "/" + retval;
	}

	// Optionally setup indenting to "pretty print"
	// Note - this is not very pretty at least in my testing - but it is better
	// than all string together
	public static String documentToString(Document document, boolean pretty)
	{
		return nodeToString(document, pretty);
	}

	// Optionally setup indenting to "pretty print"
	// Note - this is not very pretty at least in my testing - but it is better
	// than all string together
	public static String nodeToString(Node node, boolean pretty)
	{
		try {
			javax.xml.transform.Transformer tf =
				javax.xml.transform.TransformerFactory.newInstance().newTransformer();
			if ( pretty ) {
				tf.setOutputProperty(OutputKeys.INDENT, "yes");
				tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
			}
			ByteArrayOutputStream baStream = new ByteArrayOutputStream();
			tf.transform (new javax.xml.transform.dom.DOMSource (node),
					new javax.xml.transform.stream.StreamResult (baStream));
			return baStream.toString();
		} catch (javax.xml.transform.TransformerException e)  {
			return null;
		}
	}

	// Someone better at Generics can yell at me as to how this should have been 
	// done to use the same code for either objects or strings.  Sorry.
	public static Map selectSubMap(Map sm, String selection)
	{
		if ( sm == null ) return null;
		selection = selection.trim();
		if ( badSelection(selection) ) return null;
		Map retval = new TreeMap();
		selectSubMap(sm, retval, null, null, selection);
		return retval;
	}

	public static Map selectFullSubMap(Map om, String selection)
	{
		if ( om == null ) return null;
		selection = selection.trim();
		if ( badSelection(selection) ) return null;
		Map retval = new TreeMap();
		selectSubMap(null, null, om, retval, selection);
		return retval;
	}

	private static boolean badSelection(String selection)
	{
		if ( selection == null ) return true;
		if ( selection.equals("/") ) return true;
		if ( selection.length() < 2 ) return true;
		if ( ! selection.startsWith("/") ) return true;
		return false;
	}

	private static void selectSubMap(Map sm, Map sret,
			Map om,  Map oret, String selection)
	{
		Iterator iter = null;
		if ( sm != null ) {
			iter = sm.keySet().iterator();
		} else {
			iter = om.keySet().iterator();
		}

		while( iter.hasNext() ) {
			String key = iter.next();
			boolean match = false;
			String newKey = null;
			if ( key.equals(selection) ) {
				match = true;
				newKey = "/";
			} else if ( selection.endsWith("/") && key.startsWith(selection)) {
				match = true;
				newKey = key.substring(selection.length()-1);
			} else if ( key.startsWith(selection+"/") ) {
				match = true;
				newKey = key.substring(selection.length());
			} else if ( key.startsWith(selection+"!") ) {
				match = true;
				newKey = "/" + key.substring(selection.length());
			}
			if ( ! match ) continue;	
			// doDebug(d,"newKey = "+newKey);
			if ( sm != null ) {
				String value = sm.get(key);
				if ( value == null ) continue;
				sret.put(newKey,value);
				// doDebug(d,newKey+" = " + value);
			} else { 
				Object value = om.get(key);
				if ( value == null ) continue;
				oret.put(newKey,value);
				// doDebug(d,newKey+" = " + value);
			}
		}
	}

	/*
	 * Remove a submap.  Depending if the string ends ina slash - there are
	 * two behaviors.
	 * /x/y/   All of the children are removed but the node is left intact
	 * /x/y    All of the children are removed and the node itself and
	 *           any attributes are removed as well (typical case)
	 */
	public static void removeSubMap(Map tm, String selection)
	{
		if ( tm == null ) return;
		selection = selection.trim();
		if ( badSelection(selection) ) return;

		// If the selection does not end with /, generate the 
		// Attribute and children selections
		selection = selection.trim();
		String childSel = selection;
		String attrSel = selection;
		if ( ! selection.endsWith("/") ) {
			childSel = selection + "/";
			attrSel = selection + "!";
		}

		// Track what we will delete until loop is done
		Set delSet = new HashSet();

		Iterator iter = tm.keySet().iterator();
		while( iter.hasNext() ) {
			Object key = iter.next();
			if ( ! (key instanceof String) ) continue;
			String strKey = (String) key;
			if ( strKey.equals(selection) || strKey.startsWith(childSel) || strKey.startsWith(attrSel)) {
				delSet.add(strKey);
				// System.out.println("Deleting key="+key);
			}
		}

		// Actually remove...
		Iterator setIter = delSet.iterator();
		while( setIter.hasNext() ) {
			String key = setIter.next();
			tm.remove(key);
		}
	}

	private static void doDebug(int d, String str) {
		if ( ! DF ) return;
 		for(int i=0; i theMap, String key)
	{
		if ( theMap == null ) return null;
		Object obj = theMap.get(key);
		if ( obj == null ) return null;
		if ( obj instanceof String ) return (String) obj;
		return null;
	}
	
	/* This goes to a set of nodes that are intended to be multiple nodes and returns a list whether there 
	 * are one or many nodes.
	 *
	 * 
	 *   
	 *     p1keyp1val
	 *   
	 *   
	 *     p2akeyp2aval
	 *     p2bkeyp2bval
	 *   
	 * 
	 * 
	 *  List> p1s = XMLMap.getList(mnop,"/abc/p1s/p1");
	 *  List> p2s = XMLMap.getList(mnop,"/abc/p2s/p2");
	 *  
	 *  Always return a list even if it is just an empty list so this code works:
	 *  
     *          for ( Map siteMap : XMLMap.getList(mnop,"/sites/site")) {
     *          	System.out.println("Site="+siteMap);
     *          }
     */
	@SuppressWarnings("unchecked")
	public static List> getList(Map theMap,String key)
	{
		ArrayList> al = new ArrayList>();
		if ( theMap == null || key == null ) return al;
		
		// If this is a nice little list of maps - we are golden - send the list back
		Object obj = theMap.get(key);
		if ( obj instanceof List ) return (List>) obj;
		
		// We may have a single String value - we may have a single terminal value
		// perhaps with some attributes
		// 
		//   
		//      1300
		//   
		// 

		// See if there is one sub map there...
		Map oneMap = selectFullSubMap(theMap, key);
		// System.out.println("One submap = "+oneMap);
		if ( oneMap == null ) return al;
		
		// If the map is not empty - return am empty list 
		// rather than a one element list with an empty map
		if ( oneMap.isEmpty() ) return al;
		
		// Make a list of one submap...
		al.add(oneMap);
		return al;
	}
	
    // Note that getList with the first parameter to getList is a String, it does a 
	// getMap and then a getList   with that Map - this allows the following 
	// rather dense code:
	
	//  for ( Map siteMap : XMLMap.getList(xmlString,"/sites/site")) {
    
	// The long form of this looks as follows:
	
    //   Map theMap = XMLMap.getFullMap(xmlString);
    //   List> theList = XMLMap.getList(theMap, "/sites/site");
    //   for ( Map siteMap : theList) {
    
    // The short form should only be used if this is the only time you will parse
	// to get a FullMap - otherwise - get the FullMap once and pull out the different bits
	// from the map without reparsing the xmlString.

	public static List> getList(String xmlInput,String key)
	{
        Map tmpMap  = XMLMap.getFullMap(xmlInput);
        return XMLMap.getList(tmpMap,key);
	}
	
	/* 
	 * Unit Tests - Keep these public in case folks want to call them when they are
	 * only in possession of a jar file - makes the jar file a bearer instrument at
	 * the cost of some extra space.
	 */
	
	public static boolean unitTest(String xmlString, boolean doDebug)
	{

		if ( xmlString == null ) return false;
		DF = doDebug;
		
		// If Debug is turned on - let the chips fly, exceptions and
		// All...
		if ( doDebug ) {
			DF = true;
			String pretty1 = XMLMap.prettyPrint(xmlString);
			String pretty2 = XMLMap.prettyPrint(pretty1);
			if ( pretty1.equals(pretty2) ) return true;
			System.out.println("XMLMap - unit test failed");
			return false;
		}
		
		// For Debug off - we first try it silently and in a try/catch block
		DF = false;
		try {
			String pretty1 = XMLMap.prettyPrint(xmlString);
			String pretty2 = XMLMap.prettyPrint(pretty1);
			if ( pretty1.equals(pretty2) ) return true;
		}
		catch (Throwable t) {
			// We will re-do below so folks see the trace back - 
			// in the context of debug
		}

		// If we failed - re-do it with verbose mode on
		System.out.println("XMLMap - unit test failed");
		System.out.println(xmlString);
		DF = true;
		String pretty1 = XMLMap.prettyPrint(xmlString);
		System.out.println("Pretty Print Version pass 1\n"+pretty1);
		String pretty2 = XMLMap.prettyPrint(pretty1);
		System.out.println("Pretty Print Version pass 2\n"+pretty2);
		DF = false;  // Always reset class-wide variable
		return false;
	}

	// Some Unit Test and sample Strings
	private static final String simpleText = "BD";
	private static final String sitesText = "  sue   fred Title   sakai.web.content   p1key p1val   p2key p2val     sakai-wiki   wikikey     sakai-blog    ";
	private static final String rssText = "Dr-Chuck's MediaTelevision Shows and other mediahttp://www.dr-chuck.com/media.phpTrack Days with John Merlin WilliamsThis film is about racing street Motorcyles.http://www.dr-chuck.comMotocross RacingDr. Chuck comes in second to last and is covered with mud.http://www.dr-chuck.com/";
    
	public static boolean allUnitTests() {
		if ( !unitTest(simpleText, false) ) return false;
		if ( !unitTest(sitesText, false) ) return false;
		if ( !unitTest(rssText, false) ) return false;
		return true;
	}
	
	public static void main(String[] args) {
		System.out.println("Running XMLMap (www.mdom.org) unit tests..");
		if ( !allUnitTests() ) return;
		System.out.println("Unit tests passed...");
		runSamples();
	}
	
	public static void runSamples() {
		System.out.println("Running XMLMap (www.mdom.org) Samples...");
		DF = false;

		// Test the parsing of a Basic string Map
		Map tm = XMLMap.getMap(simpleText);
		// System.out.println("tm="+tm);
		
		// Test the production of a basic map
		Map simpleMap = new TreeMap();
		simpleMap.put("/a/b!x", "X");
		simpleMap.put("/a/b", "B");
		simpleMap.put("/a/c/d", "D");
		System.out.println("simpleMap\n"+simpleMap);
		String simpleXml = XMLMap.getXML(simpleMap, true);
		System.out.println("simpleXml\n"+simpleXml);
		unitTest(simpleXml,false);
				
		// Do a select of a subMap
		Map subMap = XMLMap.selectSubMap(tm, "/a/c");
		Map joinedMap = new TreeMap();
		System.out.println("subMap="+subMap);
		joinedMap.put("/top/id", "1234");
		joinedMap.put("/top/fun", subMap); // Graft the map onto this node
		System.out.println("joinedMap\n"+joinedMap);
		String joinedXml = XMLMap.getXML(joinedMap, true);
		System.out.println("joinedXML\n"+joinedXml);
		unitTest(joinedXml,false);
		
		// Do an Array
		Map arrayMap = new TreeMap();
		String [] arrayStr = { "first", "second", "third" };
        arrayMap.put("/root/stuff", arrayStr);
		System.out.println("arrayMap\n"+arrayMap);
		String arrayXml = XMLMap.getXML(arrayMap, true);
		System.out.println("arrayXml\n"+arrayXml);
		unitTest(arrayXml,false);

		// Make a Map that is a combination of Maps, String, and Arrays
        Map newMap = new TreeMap();

        newMap.put("/Root/milton","Root-milton");
        newMap.put("/Root/joe","Root-joe");
        
        Map m2 = new TreeMap();
        m2.put("/fred/a","fred-a");
        m2.put("/fred/b","fred-b");
        newMap.put("/Root/freds", m2);
        
        // Add a list of maps
        // 
        //   
        //     
        //       key-0
        //       val-0
        //     
        //     
        //       key-1
        //       val-1
        //     
        //   
        // 
        
        List> lm = new ArrayList>();
        Map m3 = null;
        m3 = new TreeMap();
        m3.put("/key","key-0");
        m3.put("/val","val-0");
        lm.add(m3);
        
        m3 = new TreeMap();
        m3.put("/key","key-1");
        m3.put("/val","val-1");
        lm.add(m3);
        
        newMap.put("/Root/maps/map", lm);
        
        // Add an array of Strings
        // 
        //   first
        //   second
        //   third
        // 
        
        String [] strar = { "first", "second", "third" };
        newMap.put("/Root/array", strar);
        
        // Add a list of Maps - this is a bit of a weird application - mostly as a 
        // completeness test to insure lists of maps and arrays are equivalent.  Also
        // since the getFullMap returns maps, not Arrays of strings, this is necessary
        // to insure symmetry - i.e. we can take a map structure we produce and 
        // regenerate the XML.  Most users will not use this form in construction.
        //
        // 
        //     item-1
        //     item-2
        // 
        
        List> l1 = new ArrayList>();
        Map m4 = new TreeMap();
        m4.put("/", "item-1");
        l1.add(m4);
        Map m5 = new TreeMap();
        m5.put("/", "item-2");
        l1.add(m5);
        newMap.put("/Root/item", l1);
 
        // Put in using the XMLMap bracket Syntax - not a particularly good
        // Way to represent multiple items - it is just here for completeness.
        newMap.put("/Root/anns/ann[0]","Root-ann[0]");
        newMap.put("/Root/anns/ann[1]","Root-ann[1]");
        newMap.put("/Root/bobs/bob[0]/key","Root-bobs-bob[0]-key");
        newMap.put("/Root/bobs/bob[0]/val","Root-bobs-bob[0]-val");
        newMap.put("/Root/bobs/bob[1]/key","Root-bobs-bob[1]-key");
        newMap.put("/Root/bobs/bob[1]/val","Root-bobs-bob[1]-val");
   
        // This is not allowed because maps cannot have duplicates 
 /*       
        Map m6 = new TreeMap();
        m5.put("/two", "two-1");
        m5.put("/two", "two-2");
        newMap.put("/Root", m6);
  */      
        
        // Take the Map - turn it into XML and then parse the returned
        // XML into a second map - take the second map and produce more XML
        // If all goes well, the two generated blobs of XML should be the
        // same.  If anything goes wrong - we re-do it with lots of debug
        String complexXml = null;
        boolean success = false;
    	DF = false;
        try {
            complexXml = XMLMap.getXML(newMap, true);
            success = true;
        } catch(Exception e) {
        	success = false;
        }
        
        // If we fail - do it again with deep levels of verbosity
        if ( success ) {
        	unitTest(complexXml,false);
        } else {
        	DF = true;
        	System.out.println("\n MISMATCH AND/OR SOME ERROR HAS OCCURED - REDO in VERBODE MODE");
            System.out.println("Starting out newMap="+newMap); 
            complexXml = XMLMap.getXML(newMap, true);
        	unitTest(complexXml,false);
        	DF = false;
        }
    	
        // A different example - iterating through nested sets - demonstrating the short form
        // of getSites() with the first parameter a string -the commented code below is the long form.
        
        // Map theMap = XMLMap.getFullMap(sitesText);
        // List> theList = XMLMap.getList(theMap, "/sites/site");
        // for ( Map siteMap : theList) {
        
        // The short form using convenience method if you don't need the map for anything else
        System.out.println("\nParsing Sites Structure");
        for ( Map siteMap : XMLMap.getList(sitesText,"/sites/site")) {
        	System.out.println("Site="+siteMap);
        	System.out.println("Id="+XMLMap.getString(siteMap,"/id"));
            for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) {
        		System.out.println("Tool="+toolMap);
        		System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid"));
                for ( Map property : XMLMap.getList(toolMap, "/properties/property")) {
        			System.out.println("key="+XMLMap.getString(property, "/key"));
        			System.out.println("val="+XMLMap.getString(property, "/val"));
        		}
        	}
	    }
        
        // Lets parse some RSS as a final kind of easy but quite practical test
        DF = false;
        System.out.println("\nParsing RSS Feed");
        // System.out.println(XMLMap.prettyPrint(rssText));
        Map rssFullMap = XMLMap.getFullMap(rssText);
        System.out.println("RSS Full Map\n"+rssFullMap);
        System.out.println("Rss Version="+XMLMap.getString(rssFullMap,"/rss!version"));
        System.out.println("Chan-desc="+XMLMap.getString(rssFullMap,"/rss/channel/description"));
        System.out.println("Chan-title="+XMLMap.getString(rssFullMap,"/rss/channel/title"));
        
        Map rssStringMap = XMLMap.flattenMap(rssFullMap);
        System.out.println("RSS Flat String Only Map\n"+rssStringMap);
        System.out.println("Rss Version="+rssStringMap.get("/rss!version"));
        System.out.println("Chan-desc="+rssStringMap.get("/rss/channel/description"));
        System.out.println("Chan-title="+rssStringMap.get("/rss/channel/title"));

        for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) {
        	System.out.println("=== Item ===");
        	System.out.println(" Item-title="+XMLMap.getString(rssItem, "/title"));
        	System.out.println(" Item-description="+XMLMap.getString(rssItem, "/description"));
        	System.out.println(" Item-link="+XMLMap.getString(rssItem, "/link"));
        }	
	}
}

/* Sample output from test run with lines wrapped a bit:

Running XMLMap (www.mdom.org) unit tests..
Unit tests passed...
Running XMLMap (www.mdom.org) Samples...
tm={/a/b=B, /a/b!x=X, /a/c/d=D}
simpleMap
{/a/b=B, /a/b!x=X, /a/c/d=D}
simpleXml


  B
  
    D
  


subMap={/d=D}
joinedMap
{/top/fun={/d=D}, /top/id=1234}
joinedXML


  
    D
  
  1234


arrayMap
{/root/stuff=[Ljava.lang.String;@6f50a8}
arrayXml


  first
  second
  third



Parsing Sites Structure
Site={/id=sue}
Id=sue
Site={/id=fred, /title=Title, /tools/tool=[{/properties/property=[{/key=p1key, /val=p1val}, 
   {/key=p2key, /val=p2val}], /toolid=sakai.web.content}, {/properties/property/key=wikikey, 
   /toolid=sakai-wiki}, {/toolid=sakai-blog}]}

Id=fred
Tool={/properties/property=[{/key=p1key, /val=p1val}, {/key=p2key, /val=p2val}], /toolid=sakai.web.content}
ToolId=sakai.web.content
key=p1key
val=p1val
key=p2key
val=p2val
Tool={/properties/property/key=wikikey, /toolid=sakai-wiki}
ToolId=sakai-wiki
key=wikikey
val=null
Tool={/toolid=sakai-blog}
ToolId=sakai-blog

Parsing RSS Feed
RSS Full Map
{/rss!version=2.0, /rss/channel/description=Television Shows and other media, 
  /rss/channel/item=[{/description=This film is about racing street Motorcyles., 
  /link=http://www.dr-chuck.com, /title=Track Days with John Merlin Williams}, 
  {/description=Dr. Chuck comes in second to last and is covered with mud., 
  /link=http://www.dr-chuck.com/, /title=Motocross Racing}], 
  /rss/channel/link=http://www.dr-chuck.com/media.php, /rss/channel/title=Dr-Chuck's Media}
   
Rss Version=2.0
Chan-desc=Television Shows and other media
Chan-title=Dr-Chuck's Media
RSS Flat String Only Map
{/rss!version=2.0, /rss/channel/description=Television Shows and other media, 
  /rss/channel/link=http://www.dr-chuck.com/media.php, /rss/channel/title=Dr-Chuck's Media}

Rss Version=2.0
Chan-desc=Television Shows and other media
Chan-title=Dr-Chuck's Media
=== Item ===
 Item-title=Track Days with John Merlin Williams
 Item-description=This film is about racing street Motorcyles.
 Item-link=http://www.dr-chuck.com
=== Item ===
 Item-title=Motocross Racing
 Item-description=Dr. Chuck comes in second to last and is covered with mud.
 Item-link=http://www.dr-chuck.com/

 */





© 2015 - 2024 Weber Informatics LLC | Privacy Policy