/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/webservices/trunk/util/src/java/org/sakaiproject/axis/util/XMLMap.java $ * $Id: XMLMap.java 118266 2013-01-10 22:00:09Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2008 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.axis.util; /* Author - Charles Severance (csev@umich.edu) */ import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.TreeMap; import java.util.Iterator; import javax.xml.XMLConstants; 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.Attr; 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 { /* * testing: * * Map<String, String> tm = XMLMap.getMap("<a><b x=\"X\">B</b><c><d>D</d></c></a>"); * System.out.println("tm="+tm); */ /* * Used when folks have XML that is a pure tree with all peer child nodes having * a different tag (D and E below are peers byt they are different tags. This * will parse but ignore addition identically named peer child nodes. * * <A> * <B>TEXT</B> * <C> * <D>D-Text</D> * <E>E-Text</E> * </C> * <F X="X-Text" /> * <G Y="Y-Text">G-Text</G> * </A> * * Output: * * /A/B TEXT * /A/C/D D-Text * /A/C/E E-Text * /A/F!X X-Text * /A/G G-Text * /A/G!Y Y-Text */ public static Map<String,String> getMap(String str) { Map<String,Object> tm = getObjectMap(str, false); if ( tm == 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 = tm.keySet().iterator(); while( iter.hasNext() ) { String key = iter.next(); Object value = tm.get(key); // No need to handle String[] - because they will not // be stored when doFull == false if ( value instanceof String ) { String svalue = (String) value; // System.out.println(key+" = " + value); if ( value != null ) retval.put(key,svalue); } } return retval; } public static Map<String,Object> getFullMap(String str) { return getObjectMap(str, true); } private static Map<String,Object> getObjectMap(String str, boolean doFull) { if ( str == null ) return null; Document doc = documentFromString(str); if ( doc == null ) return null; Map<String,Object> tm = new TreeMap<String,Object>(); recurse(tm, "", doc, doFull); return tm; } // Some nice utility methods private static Document documentFromString(String input) { try{ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder parser = builderFactory.newDocumentBuilder(); Document document = parser.parse(new ByteArrayInputStream(input.getBytes())); return document; } catch (Exception e) { return null; } } private static void recurse(Map<String, Object> tm, String path, Node parentNode, boolean doFull) { // System.out.println("path="+path+" parentNode="+ nodeToString(parentNode).getNodeName()); NodeList nl = parentNode.getChildNodes(); NamedNodeMap nm = parentNode.getAttributes(); // 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) { String value = node.getNodeValue(); if ( value == null ) break; // Might want to make this trim test selectable by user if ( value.trim().length() < 1 ) break; // System.out.println("Adding path="+path+" value="+node.getNodeValue()); tm.put(path,node.getNodeValue()); break; // Only the first one } } // TODO: Build support for doFull. // Outline: Loop throught the Element and Attribute Nodes // to see which ones are single and which are repeated // When doFull is true we handle repeated nodes with // a different syntax: // /a/b/c[0]!atr // 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(); String value = node.getNodeValue(); // System.out.println("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); } } // 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())) ) { recurse(tm, path+"/"+node.getNodeName(),node,doFull); done.add(node.getNodeName()); } } } public static String getXML(Map tm) { Document document = getXMLDom(tm); if ( document == null ) return null; return documentToString(document, false); } 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 = prettyString(retval); } return retval; } public static String prettyString(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{ DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); document = parser.newDocument(); } catch (Exception e) { return null; } iterateMap(document, document.getDocumentElement(), tm); 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) { // System.out.println("> IterateMap parentNode = "+ nodeToString(parentNode)); Iterator iter = tm.keySet().iterator(); while( iter.hasNext() ) { String key = (String) iter.next(); if ( ! key.startsWith("/") ) continue; // Skip Object obj = tm.get(key); if ( obj instanceof String ) { storeInDom(document, parentNode, key, (String) obj); } else if ( obj instanceof Map ) { Map subMap = (Map) obj; Node startNode = getNodeAtPath(document, parentNode, key); // System.out.println("descending into path="+key+" startNode="+ nodeToString(startNode)); iterateMap(document, startNode, subMap); // System.out.println("back from descent path="+key+" startNode="+ nodeToString(startNode)); } } // System.out.println("< IterateMap parentNode = "+ nodeToString(parentNode)); } private static void storeInDom(Document document, Node parentNode, String key, String value) { // System.out.println("> storeInDom"+key+" = " + value + " parent="+ nodeToString(parentNode)); if ( document == null | key == null || value == null ) return; if ( parentNode == null ) parentNode = document; // System.out.println("parentNode I="+ nodeToString(parentNode)); String [] newPath = key.split("/"); // System.out.println("newPath = "+outStringArray(newPath)); String nodeAttr = null; for ( int i=1; i< newPath.length; i++ ) { String nodeName = newPath[i]; if ( i == newPath.length-1 ) { // System.out.println("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]; // System.out.println("new nodeName="+nodeName+" nodeAttr="+nodeAttr); } } parentNode = getOrAddChildNode(document, parentNode, nodeName); } // System.out.println("parentNode after="+ nodeToString(parentNode)); if ( nodeAttr != null ) { if ( value!= null && parentNode instanceof Element ) { Element element = (Element) parentNode; // System.out.println("Adding an attribute "+nodeAttr); element.setAttribute(nodeAttr,value); } } else if ( value != null ) { Text newNode = document.createTextNode(value); parentNode.appendChild(newNode); } // System.out.println("xml="+documentToString(document,false)); // System.out.println("< 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) { if ( parentNode == null ) parentNode = document; // System.out.println("> getNodeAtPath path="+path+" parentNode="+ nodeToString(parentNode)); String [] newPath = path.split("/"); // System.out.println("newPath = "+outStringArray(newPath)); for ( int i=1; i< newPath.length; i++ ) { String nodeName = newPath[i]; if ( i == newPath.length-1 ) { // System.out.println("Splitting !="+nodeName); // check to see if we have a nodename=attributename String [] nodeSplit = nodeName.split("!"); if ( nodeSplit.length > 1 ) { nodeName = nodeSplit[0]; // System.out.println("new nodeName="+nodeName); } } parentNode = getOrAddChildNode(document, parentNode, nodeName); } // System.out.println("< getNodeAtPath returning="+ nodeToString(parentNode)); return parentNode; } private static Node getOrAddChildNode(Document doc, Node parentNode, String nodeName) { // System.out.println("> getOrAddChildNode name="+nodeName+" parentNode="+ nodeToString(parentNode)); if ( nodeName == null || parentNode == null) return null; NodeList nl = parentNode.getChildNodes(); if ( nl != null ) for (int i = 0; i< nl.getLength(); i++ ) { Node node = nl.item(i); // System.out.println("length= " +nl.getLength()+ " i="+i+" NT="+node.getNodeType()); // System.out.println("searching nn="+nodeName+" nc="+node.getNodeName()); if (node.getNodeType() == node.ELEMENT_NODE) { if ( nodeName.equals(node.getNodeName()) ) { // System.out.println("< getOrAddChildNode found retval="+ nodeToString(node)); return node; } } } Element newNode = doc.createElement(nodeName); parentNode.appendChild(newNode); // System.out.println("Adding "+nodeName+" at "+ nodeToString(parentNode)+" in "+doc); // System.out.println("xml="+documentToString(doc,false)); // System.out.println("< getOrAddChildNode added retval="+ 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) { 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 (document), new javax.xml.transform.stream.StreamResult (baStream)); return baStream.toString(); } catch (Exception 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 obects or strings. Sorry. public static Map<String, String> selectSubMap(Map<String, String> sm, String selection) { if ( sm == null ) return null; selection = selection.trim(); if ( ! selection.endsWith("/") ) selection = selection + "/"; 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 ( ! selection.endsWith("/") ) selection = selection + "/"; 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 ( "/".equals(selection) ) 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(); if ( key.startsWith(selection) ) { String newKey = key.substring(selection.length()-1); // System.out.println("newKey = "+newKey); if ( sm != null ) { String value = sm.get(key); if ( value == null ) continue; sret.put(newKey,value); // System.out.println(newKey+" = " + value); } else { Object value = om.get(key); if ( value == null ) continue; oret.put(newKey,value); // System.out.println(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); } } }