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

org.tsugi.basiclti.XMLMap Maven / Gradle / Ivy

There is a newer version: 23.3
Show newest version
/**********************************************************************************
 * $URL$
 * $Id$
 **********************************************************************************
 *
 * Copyright (c) 2009-2016 Charles R. Severance
 *
 * 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.tsugi.basiclti;

/* 
 * 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.
 * 
 *   log.debug("{}", 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();
 *	log.debug("subMap={}", subMap);
 *	joinedMap.put("/top/id", "1234");
 *	joinedMap.put("/top/fun", subMap); // Graft the map onto this node
 *	String joinedXml = XMLMap.getXML(joinedMap, true);
 *	log.debug("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);
 *   log.debug("Rss Version={}", XMLMap.getString(rssFullMap,"/rss!version"));
 *   log.debug("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);
 *   log.debug("Rss Version={}", rssStringMap.get("/rss!version"));
 *   log.debug("Chan-title={}", rssStringMap.get("/rss/channel/title"));
 *   
 * Iterating through a Full Map is pretty easy:

 *   for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) {
 *      log.debug("=== Item ===");
 *      log.debug(" 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) {
 *     log.debug("Id={}", XMLMap.getString(siteMap,"/id"));
 *     for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) {
 *        log.debug("ToolId={}", XMLMap.getString(toolMap,"/toolid"));
 *        for ( Map property : XMLMap.getList(toolMap, "/properties/property")) {
 *       	log.debug("key={}", XMLMap.getString(property, "/key"));
 *        	log.debug("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")) {
 *     log.debug("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 lombok.extern.slf4j.Slf4j;

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.
 */
@Slf4j
public class XMLMap {	
	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) 
	{
		log.debug("> recurse path={} parentNode={}", path, 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;
				log.debug("Adding path={} value={}", path, 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();
				log.debug("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())) ) {
					log.debug("Going down the rabbit hole path={} node={}", path, node.getNodeName());
					recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d);
					log.debug("Back from the rabbit hole path={} node={}", path, node.getNodeName());
					done.add(node.getNodeName());	
				}
			}
			d--;
			log.debug("< recurse path={} parentNode={}", path, 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;
			log.debug("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 ) {
					log.debug("Going down the single rabbit hole path={} node={}", path, node.getNodeName());
					recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d);
					log.debug("Back from the single rabbit hole path={} node={}", path, node.getNodeName());
				} else {
					log.debug("Going down the multi rabbit hole path={} node={}", path, node.getNodeName());
					Map newMap = new TreeMap();
					recurse(newMap,"/",node,doFull,d);
					log.debug("Back from the multi rabbit hole path={} node={} map={}", path, node.getNodeName(), 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;
			log.debug("Adding sub-map name={} list={}", nextChild, newList);
			tm.put(path+"/"+nextChild, newList);
		}
		d--;
		log.debug("< recurse path={} parentNode={}", path, 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)
	{
		log.debug("> 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;
				log.debug("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);
				log.debug("descending into Map path={} startNode={}", key, nodeToString(startNode));
				iterateMap(document, startNode, subMap, d);
				log.debug("back from descent Map path={} startNode={}", key, nodeToString(startNode));
			} else if ( obj instanceof List ) {
				List lst = (List) obj;
				log.debug("Have a list that is this long {}", lst.size());
				Iterator listIter = lst.iterator();
				int newPos = 0;
				while ( listIter.hasNext() ) {
					Object listObj = listIter.next();
					log.debug("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;
						log.debug("Retrieving key from  List-Map path={}@{}", key, newPos);
						Node startNode = getNodeAtPath(document, parentNode, key, newPos, d);
						log.debug("descending into List-Map path={}@{} startNode={}", key, newPos, nodeToString(startNode));
						iterateMap(document, startNode, subMap, d);
						log.debug("back from descent List-Map path={}@{} startNode={}", key, newPos, nodeToString(startNode));
						newPos++;
					} else {
						log.info("XMLMap Encountered an object of type {} in a List which should contain only Map objects", obj.getClass().getName());
					}
				}
 			} else {
				log.debug("Found a {} do not know how to iterate.", obj.getClass().getName());
			}
		}
		d--;
		log.debug("< IterateMap parentNode = {}", nodeToString(parentNode));
	}

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

		String [] newPath = key.split("/");
		log.debug("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;
		log.debug("> getNodeAtPath path@{}={} parentNode={}", nodePos, path, 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--;
		log.debug("< getNodeAtPath returning={}", nodeToString(parentNode));
		return parentNode;
	}

	@SuppressWarnings("static-access")
	private static Node getOrAddChildNode(Document doc, Node parentNode, String nodeName,int whichNode, int d)
	{
		log.debug("> getOrAddChildNode name={}@{} parentNode={}", nodeName, whichNode, 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);
			log.debug("Index String = {}", indStr);
			nodeName = nodeName.substring(0,begpos);
			log.debug("New Nodename={}", nodeName);
			Integer iVal = new Integer(indStr); 
			log.debug("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--;
					log.debug("< getOrAddChildNode found name={}", nodeToString(node));
					log.debug("foundNodes = {} looking for node={}", foundNodes, whichNode);
					if ( foundNodes >= whichNode ) return node;
				}
			}
		}

		Element newNode = null;
		while ( foundNodes < whichNode ) {
			foundNodes++;
			log.debug("Adding node at position {} moving toward {}", foundNodes, whichNode);
			if ( nodeName == null ) continue;
			newNode = doc.createElement(nodeName);
			log.debug("Adding {} at {} in {}", nodeName, nodeToString(parentNode), doc);
			parentNode.appendChild(newNode);
			log.debug("xml={}", documentToString(doc,false));
			log.debug("getOrAddChildNode added newnode={}", nodeToString(newNode));
		}
		d--;
		log.debug("< 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);
				log.debug("Deleting key={}", key);
			}
		}

		// Actually remove...
		Iterator setIter = delSet.iterator();
		while( setIter.hasNext() ) {
			String key = setIter.next();
			tm.remove(key);
		}
	}
	
	//  Assume the Object is a String - get it or return null if it is anything but a String
	public static String getString(Map 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")) {
     *          	log.debug("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);
		log.debug("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;
		
		// If Debug is turned on - let the chips fly, exceptions and
		// All...
		if ( doDebug ) {
			String pretty1 = XMLMap.prettyPrint(xmlString);
			String pretty2 = XMLMap.prettyPrint(pretty1);
			if ( pretty1.equals(pretty2) ) return true;
			log.debug("XMLMap - unit test failed");
			return false;
		}
		
		// For Debug off - we first try it silently and in a try/catch block
		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
		log.debug("XMLMap - unit test failed");
		log.debug(xmlString);
		String pretty1 = XMLMap.prettyPrint(xmlString);
		log.debug("Pretty Print Version pass 1\n{}", pretty1);
		String pretty2 = XMLMap.prettyPrint(pretty1);
		log.debug("Pretty Print Version pass 2\n{}", pretty2);
		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) {
		log.debug("Running XMLMap (www.mdom.org) unit tests..");
		if ( !allUnitTests() ) return;
		log.debug("Unit tests passed...");
		runSamples();
	}
	
	public static void runSamples() {
		log.debug("Running XMLMap (www.mdom.org) Samples...");

		// Test the parsing of a Basic string Map
		Map tm = XMLMap.getMap(simpleText);
		log.debug("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");
		log.debug("simpleMap\n{}", simpleMap);
		String simpleXml = XMLMap.getXML(simpleMap, true);
		log.debug("simpleXml\n{}", simpleXml);
		unitTest(simpleXml,false);
				
		// Do a select of a subMap
		Map subMap = XMLMap.selectSubMap(tm, "/a/c");
		Map joinedMap = new TreeMap();
		log.debug("subMap={}", subMap);
		joinedMap.put("/top/id", "1234");
		joinedMap.put("/top/fun", subMap); // Graft the map onto this node
		log.debug("joinedMap\n{}", joinedMap);
		String joinedXml = XMLMap.getXML(joinedMap, true);
		log.debug("joinedXML\n{}", joinedXml);
		unitTest(joinedXml,false);
		
		// Do an Array
		Map arrayMap = new TreeMap();
		String [] arrayStr = { "first", "second", "third" };
        arrayMap.put("/root/stuff", arrayStr);
		log.debug("arrayMap\n{}", arrayMap);
		String arrayXml = XMLMap.getXML(arrayMap, true);
		log.debug("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;
        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 {
        	log.debug("\n MISMATCH AND/OR SOME ERROR HAS OCCURED - REDO in VERBODE MODE");
        	log.debug("Starting out newMap={}", newMap); 
            complexXml = XMLMap.getXML(newMap, true);
        	unitTest(complexXml,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
        log.debug("\nParsing Sites Structure");
        for ( Map siteMap : XMLMap.getList(sitesText,"/sites/site")) {
        	log.debug("Site={}", siteMap);
        	log.debug("Id={}", XMLMap.getString(siteMap,"/id"));
            for ( Map toolMap : XMLMap.getList(siteMap,"/tools/tool")) {
        		log.debug("Tool={}", toolMap);
        		log.debug("ToolId={}", XMLMap.getString(toolMap,"/toolid"));
                for ( Map property : XMLMap.getList(toolMap, "/properties/property")) {
        			log.debug("key={}", XMLMap.getString(property, "/key"));
        			log.debug("val={}", XMLMap.getString(property, "/val"));
        		}
        	}
	    }

        log.debug("\nParsing RSS Feed");
        log.debug(XMLMap.prettyPrint(rssText));
        Map rssFullMap = XMLMap.getFullMap(rssText);
        log.debug("RSS Full Map\n{}", rssFullMap);
        log.debug("Rss Version={}", XMLMap.getString(rssFullMap,"/rss!version"));
        log.debug("Chan-desc={}", XMLMap.getString(rssFullMap,"/rss/channel/description"));
        log.debug("Chan-title={}", XMLMap.getString(rssFullMap,"/rss/channel/title"));
        
        Map rssStringMap = XMLMap.flattenMap(rssFullMap);
        log.debug("RSS Flat String Only Map\n{}", rssStringMap);
        log.debug("Rss Version={}", rssStringMap.get("/rss!version"));
        log.debug("Chan-desc={}", rssStringMap.get("/rss/channel/description"));
        log.debug("Chan-title={}", rssStringMap.get("/rss/channel/title"));

        for ( Map rssItem : XMLMap.getList(rssFullMap,"/rss/channel/item")) {
        	log.debug("=== Item ===");
        	log.debug(" Item-title={}", XMLMap.getString(rssItem, "/title"));
        	log.debug(" Item-description={}", XMLMap.getString(rssItem, "/description"));
        	log.debug(" 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