/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/basiclti/XMLMap.java $ * $Id: XMLMap.java 131989 2013-11-27 01:36:00Z csev@umich.edu $ ********************************************************************************** * * 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.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: * * <a> * <b x="X">B</b> * <c> * <d>D</d> * </c> * </a> * * Map<String, String> xmlMap = XMLMap.getMap("<a><b x=\"X\">B</b><c><d>D</d></c></a>"); * * 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<String,String> newMap = new TreeMap<String,String>; * 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<String,String> subMap = XMLMap.selectSubMap(tm, "/a/c"); * Map<String,Object> joinedMap = new TreeMap<String,Object>(); * 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: * * <top> * <fun> * <d>D</d> * </fun> * <id>1234</id> * </top> * * 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<String,Object> you can even add an array of strings to an entry. * * Map<String,Object> arrayMap = new TreeMap<String,Object>(); * String [] strar = { "first", "second", "third" }; * arrayMap.put("/root/stuff", strar); * String arrayXml = XMLMap.getXML(arrayMap, true); * * Produces: * * <root> * <stuff>first</stuff> * <stuff>second</stuff> * <stuff>third</stuff> * </root> * * 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<Map<String,Object>> 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 <String,Object>: * * Map<String,Object> 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<String,String> and use get to lookup * * Map<String,String> 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<String,Object> 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<Map<String,Object>> 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<String,Object> theMap = XMLMap.getFullMap(bob); * List<Map<String,Object>> theList = XMLMap.getList(theMap, "/sites/site"); * for ( Map<String,Object> siteMap : theList) { * System.out.println("Id="+XMLMap.getString(siteMap,"/id")); * for ( Map<String,Object> toolMap : XMLMap.getList(siteMap,"/tools/tool")) { * System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid")); * for ( Map<String,Object> 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<String,Object> 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 debugFlag = false; public static Map<String,String> getMap(String str) { if ( str == null ) return null; Document doc = documentFromString(str); if ( doc == null ) return null; return getMap(doc); } public static Map<String,String> getMap(Node doc) { Map<String,Object> tm = getObjectMap(doc, false); if ( tm == null ) return null; return flattenMap(tm); } public static Map<String,String> flattenMap(Map<String,Object> theMap) { if ( theMap == null ) return null; // Reduce to the first column of elements for the simple return value TreeMap<String,String> retval = new TreeMap<String, String> (); Iterator<String> 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<String,Object> getFullMap(Node doc) { return getObjectMap(doc, true); } public static Map<String,Object> getFullMap(String str) { if ( str == null ) return null; Document doc = documentFromString(str); if ( doc == null ) return null; return getObjectMap(doc, true); } private static Map<String,Object> getObjectMap(Node doc, boolean doFull) { if ( doc == null ) return null; Map<String,Object> tm = new TreeMap<String,Object>(); 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<String, Object> tm, String path, Node parentNode, boolean doFull, int d) { 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 <String> done = new HashSet<String>(); 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())) ) { doDebug(d,"Going down the rabbit hole path="+path+" node="+node.getNodeName()); recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d); doDebug(d,"Back from the rabbit hole path="+path+" node="+node.getNodeName()); done.add(node.getNodeName()); } } d--; 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<String,Integer> childMap = new TreeMap<String,Integer>(); 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<String> iter = childMap.keySet().iterator(); Map<String,List<Map<String,Object>>> nodeMap = new TreeMap<String,List<Map<String,Object>>>(); while ( iter.hasNext() ) { String nextChild = iter.next(); if ( nextChild == null ) continue; Integer count = childMap.get(nextChild); if ( count == null ) continue; if ( count < 2 ) continue; doDebug(d,"Making a List for "+nextChild); List<Map<String,Object>> newList = new ArrayList<Map<String,Object>>(); 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<Map<String,Object>> mapList = nodeMap.get(childName); if ( mapList == null ) { doDebug(d,"Going down the single rabbit hole path="+path+" node="+node.getNodeName()); recurse(tm, addSlash(path)+node.getNodeName(),node,doFull,d); doDebug(d,"Back from the single rabbit hole path="+path+" node="+node.getNodeName()); } else { doDebug(d,"Going down the multi rabbit hole path="+path+" node="+node.getNodeName()); Map<String,Object> newMap = new TreeMap<String,Object>(); recurse(newMap,"/",node,doFull,d); 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<String> iter2 = nodeMap.keySet().iterator(); while ( iter2.hasNext() ) { String nextChild = iter2.next(); if ( nextChild == null ) continue; List<Map<String,Object>> newList = nodeMap.get(nextChild); if ( newList == null ) continue; if ( newList.size() < 1 ) continue; doDebug(d,"Adding sub-map name="+nextChild+" list="+newList); tm.put(path+"/"+nextChild, newList); } d--; 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("<?xml") ) { int pos = retval.indexOf("<",1); if ( pos > 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<String, Object> 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<inString.length(); i++ ) { char ch = inString.charAt(i); char nc = ' '; if ( (i+1) < inString.length() ) nc = inString.charAt(i+1); if ( ch == '\n' ) { sb.append('\n'); newLine = true; continue; } // Eat Leading whitespace if ( newLine && ( ch == ' ' || ch == '\t' ) ) continue; // Decrement depth if the first non-space is an end-tag if ( ch == '<' && nc == '/' ) depth--; if ( newLine ) { for (int j=0; j<depth && j < 15; j++) sb.append(" "); newLine = false; } // Update depth if necessary if ( ch == '<' && ! ( nc == '/' || nc == '?' )) depth++; sb.append(ch); } return sb.toString(); } public static Document getXMLDom(Map<?, ?> 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 <a> <b>B1</b> <c q="Q1"> <x>X1</x> <y r="R1">Y1</y> </c> <d>D1</d> </a> */ private static void iterateMap(Document document, Node parentNode, Map<?, ?> tm, int d) { 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; 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); doDebug(d,"descending into Map path="+key+" startNode="+ nodeToString(startNode)); iterateMap(document, startNode, subMap, d); doDebug(d,"back from descent Map path="+key+" startNode="+ nodeToString(startNode)); } else if ( obj instanceof List ) { List<?> lst = (List<?>) obj; 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(); 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; doDebug(d,"Retrieving key from List-Map path="+key+"@"+newPos); Node startNode = getNodeAtPath(document, parentNode, key, newPos, d); doDebug(d,"descending into List-Map path="+key+"@"+newPos+" startNode="+ nodeToString(startNode)); iterateMap(document, startNode, subMap, d); 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 { doDebug(d,"Found a "+obj.getClass().getName()+" do not know how to iterate."); } } d--; doDebug(d,"< IterateMap parentNode = "+ nodeToString(parentNode)); } private static void storeInDom(Document document, Node parentNode, String key, String value, int nodePos, int d) { doDebug(d,"> storeInDom"+key+"@"+ nodePos + " = " + value + " parent="+ nodeToString(parentNode)); d++; if ( document == null || key == null || value == null ) return; if ( parentNode == null ) parentNode = document; doDebug(d,"parentNode I="+ nodeToString(parentNode)); String [] newPath = key.split("/"); 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; 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--; 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) { 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); doDebug(d,"Index String = "+ indStr); nodeName = nodeName.substring(0,begpos); doDebug(d,"New Nodename="+nodeName); Integer iVal = new Integer(indStr); 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--; doDebug(d,"< getOrAddChildNode found name="+ nodeToString(node)); doDebug(d,"foundNodes = "+foundNodes+" looking for node="+whichNode); if ( foundNodes >= whichNode ) return node; } } } Element newNode = null; while ( foundNodes < whichNode ) { foundNodes++; doDebug(d,"Adding node at position " + foundNodes + " moving toward " + whichNode); if ( nodeName == null ) continue; newNode = doc.createElement(nodeName); doDebug(d,"Adding "+nodeName+" at "+ nodeToString(parentNode)+" in "+doc); parentNode.appendChild(newNode); doDebug(d,"xml="+documentToString(doc,false)); doDebug(d,"getOrAddChildNode added newnode="+ nodeToString(newNode)); } d--; 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<String, String> selectSubMap(Map<String, String> sm, String selection) { if ( sm == null ) return null; selection = selection.trim(); if ( badSelection(selection) ) return null; Map<String, String> retval = new TreeMap<String, String>(); selectSubMap(sm, retval, null, null, selection); return retval; } public static Map<String, Object> selectFullSubMap(Map<String, Object> om, String selection) { if ( om == null ) return null; selection = selection.trim(); if ( badSelection(selection) ) return null; Map<String, Object> retval = new TreeMap<String, Object>(); 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<String, String> sm, Map<String, String> sret, Map<String, Object> om, Map<String, Object> oret, String selection) { Iterator<String> 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<String> delSet = new HashSet<String>(); 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<String> setIter = delSet.iterator(); while( setIter.hasNext() ) { String key = setIter.next(); tm.remove(key); } } private static void doDebug(int d, String str) { if ( ! debugFlag ) return; for(int i=0; i<d;i++) System.out.print(" "); System.out.println(str); } // Assume the Object is a String - get it or return null if it is anything but a String public static String getString(Map<String,Object> 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. * * <abc> * <p1s> * <p1><key>p1key</key><val>p1val</val></p1> * </p1s> * <p2s> * <p2><key>p2akey</key><val>p2aval</val></p2> * <p2><key>p2bkey</key><val>p2bval</val></p2> * </p2s> * </abc> * * List<Map<String,Object>> p1s = XMLMap.getList(mnop,"/abc/p1s/p1"); * List<Map<String,Object>> 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<String,Object> siteMap : XMLMap.getList(mnop,"/sites/site")) { * System.out.println("Site="+siteMap); * } */ @SuppressWarnings("unchecked") public static List<Map<String,Object>> getList(Map<String,Object> theMap,String key) { ArrayList<Map<String, Object>> al = new ArrayList<Map<String, Object>>(); 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<Map<String,Object>>) obj; // We may have a single String value - we may have a single terminal value // perhaps with some attributes // <toolInstance> // <tool-settings> // <setting key="frameheight">1300</setting> // </tool-settings> // </toolInstance> // See if there is one sub map there... Map<String, Object> 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<String,Object> siteMap : XMLMap.getList(xmlString,"/sites/site")) { // The long form of this looks as follows: // Map<String,Object> theMap = XMLMap.getFullMap(xmlString); // List<Map<String,Object>> theList = XMLMap.getList(theMap, "/sites/site"); // for ( Map<String,Object> 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<Map<String,Object>> getList(String xmlInput,String key) { Map<String,Object> 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; debugFlag = doDebug; // If Debug is turned on - let the chips fly, exceptions and // All... if ( doDebug ) { debugFlag = 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 debugFlag = 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); debugFlag = 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); debugFlag = false; // Always reset class-wide variable return false; } // Some Unit Test and sample Strings private static final String simpleText = "<a><b x=\"X\">B</b><c><d>D</d></c></a>"; private static final String sitesText = "<sites> <site> <id>sue</id> </site> <site> <id>fred</id> <title>Title</title> <tools> <tool> <toolid>sakai.web.content</toolid> <properties> <property> <key>p1key</key> <val>p1val</val> </property> <property> <key>p2key</key> <val>p2val</val> </property> </properties> </tool> <tool> <toolid>sakai-wiki</toolid> <properties> <property> <key>wikikey</key> </property> </properties> </tool> <tool> <toolid>sakai-blog</toolid> </tool> </tools> </site> </sites>"; private static final String rssText = "<rss version=\"2.0\"><channel><title>Dr-Chuck's Media</title><description>Television Shows and other media</description><link>http://www.dr-chuck.com/media.php</link><item><title>Track Days with John Merlin Williams</title><description>This film is about racing street Motorcyles.</description><link>http://www.dr-chuck.com</link></item><item><title>Motocross Racing</title><description>Dr. Chuck comes in second to last and is covered with mud.</description><link>http://www.dr-chuck.com/</link></item></channel></rss>"; 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..."); debugFlag = false; // Test the parsing of a Basic string Map Map<String, String> tm = XMLMap.getMap(simpleText); // System.out.println("tm="+tm); // Test the production of a basic map Map<String,String> simpleMap = new TreeMap<String,String>(); 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<String,String> subMap = XMLMap.selectSubMap(tm, "/a/c"); Map<String,Object> joinedMap = new TreeMap<String,Object>(); 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<String,Object> arrayMap = new TreeMap<String,Object>(); 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<String,Object> newMap = new TreeMap<String,Object>(); newMap.put("/Root/milton","Root-milton"); newMap.put("/Root/joe","Root-joe"); Map<String,String> m2 = new TreeMap<String,String>(); m2.put("/fred/a","fred-a"); m2.put("/fred/b","fred-b"); newMap.put("/Root/freds", m2); // Add a list of maps // <Root> // <maps> // <map> // <key>key-0</key> // <val>val-0</val> // </map> // <map> // <key>key-1</key> // <val>val-1</val> // </map> // </maps> // </Root> List<Map<String,String>> lm = new ArrayList<Map<String,String>>(); Map<String,String> m3 = null; m3 = new TreeMap<String,String>(); m3.put("/key","key-0"); m3.put("/val","val-0"); lm.add(m3); m3 = new TreeMap<String,String>(); m3.put("/key","key-1"); m3.put("/val","val-1"); lm.add(m3); newMap.put("/Root/maps/map", lm); // Add an array of Strings // <Root> // <array>first</array> // <array>second</array> // <array>third</array> // </Root> 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. // // <Root> // <item>item-1</item> // <item>item-2</item> // </Root> List<Map<String,String>> l1 = new ArrayList<Map<String,String>>(); Map<String,String> m4 = new TreeMap<String,String>(); m4.put("/", "item-1"); l1.add(m4); Map<String,String> m5 = new TreeMap<String,String>(); 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<String,String> m6 = new TreeMap<String,String>(); 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; debugFlag = 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 { debugFlag = 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); debugFlag = 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<String,Object> theMap = XMLMap.getFullMap(sitesText); // List<Map<String,Object>> theList = XMLMap.getList(theMap, "/sites/site"); // for ( Map<String,Object> 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<String,Object> siteMap : XMLMap.getList(sitesText,"/sites/site")) { System.out.println("Site="+siteMap); System.out.println("Id="+XMLMap.getString(siteMap,"/id")); for ( Map<String,Object> toolMap : XMLMap.getList(siteMap,"/tools/tool")) { System.out.println("Tool="+toolMap); System.out.println("ToolId="+XMLMap.getString(toolMap,"/toolid")); for ( Map<String,Object> 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 debugFlag = false; System.out.println("\nParsing RSS Feed"); // System.out.println(XMLMap.prettyPrint(rssText)); Map<String,Object> 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<String,String> 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<String,Object> 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 <?xml version="1.0" encoding="UTF-8"?> <a> <b x="X">B</b> <c> <d>D</d> </c> </a> subMap={/d=D} joinedMap {/top/fun={/d=D}, /top/id=1234} joinedXML <?xml version="1.0" encoding="UTF-8"?> <top> <fun> <d>D</d> </fun> <id>1234</id> </top> arrayMap {/root/stuff=[Ljava.lang.String;@6f50a8} arrayXml <?xml version="1.0" encoding="UTF-8"?> <root> <stuff>first</stuff> <stuff>second</stuff> <stuff>third</stuff> </root> 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/ */