001 package edu.nrao.sss.util; 002 003 import java.beans.PropertyChangeListener; 004 import java.beans.PropertyChangeSupport; 005 import java.io.IOException; 006 import java.lang.reflect.InvocationHandler; 007 import java.lang.reflect.Method; 008 import java.lang.reflect.Proxy; 009 import java.text.ParseException; 010 import java.text.SimpleDateFormat; 011 import java.util.Date; 012 import java.util.HashMap; 013 import java.util.Map; 014 import java.util.ArrayList; 015 import java.util.List; 016 017 import javax.xml.parsers.ParserConfigurationException; 018 import javax.xml.xpath.XPath; 019 import javax.xml.xpath.XPathConstants; 020 import javax.xml.xpath.XPathExpression; 021 import javax.xml.xpath.XPathExpressionException; 022 import javax.xml.xpath.XPathFactory; 023 024 import org.w3c.dom.DOMException; 025 import org.w3c.dom.DOMImplementation; 026 import org.w3c.dom.Document; 027 import org.w3c.dom.Node; 028 import org.w3c.dom.NodeList; 029 import org.w3c.dom.ls.DOMImplementationLS; 030 import org.w3c.dom.ls.LSException; 031 import org.w3c.dom.ls.LSSerializer; 032 import org.xml.sax.InputSource; 033 import org.xml.sax.SAXException; 034 035 /** 036 * This is a base class for simple Java POJOs that reflect an XML file. 037 */ 038 public class XmlWrapper implements InvocationHandler { 039 private Node xml; 040 private XPath xpath; 041 private Map<String, Tuple> values; 042 private boolean committed = false; 043 044 public static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 045 public static final SimpleDateFormat ISO_8601_s = new SimpleDateFormat("yyyy-MM-dd"); 046 047 protected static class Tuple { 048 public List<String> values; 049 public XPathExpression xpath; 050 public boolean dirty; 051 } 052 053 public List<String> get(String parameter) { 054 Tuple v = values.get(parameter); 055 return (v==null) ? null : v.values; 056 } 057 058 public void set(String parameter, List<String> list) { 059 Tuple v = values.get(parameter); 060 if( v != null ) { 061 v.values = list; 062 v.dirty = true; 063 committed = false; 064 } 065 } 066 067 // pushes the values<> to the backing xml Nodes 068 public void commit() throws XPathExpressionException { 069 if( ! committed ) { 070 for( String k: values.keySet()){ 071 Tuple v = values.get(k); 072 if( v != null && v.dirty ) { 073 XPathExpression xpr = v.xpath; 074 075 if(xpr != null) { 076 Node xprNode = (Node) xpr.evaluate(xml, XPathConstants.NODE); 077 if( xprNode!= null ) { 078 switch (xprNode.getNodeType()) { 079 case Node.ATTRIBUTE_NODE: 080 case Node.TEXT_NODE: 081 if (v.values != null && v.values.size() > 0) 082 xprNode.setNodeValue(v.values.get(0)); 083 break; 084 case Node.ELEMENT_NODE: 085 if ( xprNode.hasChildNodes()){ 086 NodeList children = xprNode.getChildNodes(); 087 for (int i = 0; i < children.getLength(); i++){ 088 Node child = children.item(i); 089 if ( child.getNodeType() == Node.TEXT_NODE ){ 090 child.setNodeValue(v.values.get(i)); 091 } 092 } 093 } 094 else { 095 // see comment at the end of this function! 096 if (v.values != null && v.values.size() > 0) 097 xprNode.setTextContent(v.values.get(0)); 098 } 099 break; 100 } 101 } 102 } 103 v.dirty = false; 104 } 105 } 106 } 107 } 108 109 /* NOTE: 110 * setTextContent() will DESTROY any child nodes in an XML document and replace them 111 * with the literal string value. 112 * 113 * This is a dangerous operation, but this commit() assumes that we know what we're 114 * doing. For mixed-content elements, the safer thing would be to change just the 115 * text content and leave other children alone, with code like this: 116 * 117 * // Set the first text node without bothering other children 118 * if (xprNode.hasChildNodes()) { 119 * // get the first child that is a text node and set it 120 * NodeList children = xprNode.getChildNodes(); 121 * for( int i = 0; i < children.getLength(); i++) { 122 * Node child = children.item(i); 123 * if( child.getNodeType() == Node.TEXT_NODE ) { 124 * child.setNodeValue(value); 125 * break; 126 * } 127 * } 128 * } else // no children 129 * 130 * which would replace the comment line in the case Node.ELEMENT_NODE, above. 131 * 132 * I tried some use cases with my own code, and I never try to set a string value 133 * of a mixed-content XML tag. So I took the code out. 134 * 135 * he code in this comment is more general and 136 * should work. But it will prevent you from replacing the XML content of mixed-content 137 * nodes: if you wish to set the XML data of a node, you can so so with the simple 138 * implementation, above, by passing XML fragments in as the value. You cannot do this 139 * with the safer code... 140 * To sum up, the one-line code above is more consistent behaviour and does not try 141 * to hide the underlying XML nature from the users of this class. 142 */ 143 144 145 private void setPath(String parameter, String xpathExpr) throws XPathExpressionException { 146 Tuple v = new Tuple(); 147 XPathExpression xpr = xpath.compile(xpathExpr); 148 NodeList list = (NodeList) xpr.evaluate(xml, XPathConstants.NODESET); 149 150 if (list != null) 151 { 152 int len = list.getLength(); 153 v.values = new ArrayList<String>(); 154 155 for (int i = 0; i < len; i++) 156 { 157 v.values.add(list.item(i).getTextContent()); 158 } 159 160 v.xpath = xpr; 161 v.dirty = false; 162 163 values.put(parameter, v); 164 } 165 } 166 167 protected void fillParameters(String[][] stringArray) throws XPathExpressionException{ 168 for(String[] entry: stringArray) { 169 String parameter = entry[0]; 170 String xpathExpr = entry[1]; 171 setPath(parameter, xpathExpr); 172 } 173 } 174 175 public XmlWrapper(String uri) throws SAXException, IOException, ParserConfigurationException { 176 xml = javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(uri); 177 xpath = XPathFactory.newInstance().newXPath(); 178 values = new HashMap<String,Tuple>(); 179 } 180 181 public XmlWrapper(String uri, String root) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { 182 this(uri); 183 xml = (Node) xpath.evaluate(root, xml, XPathConstants.NODE); 184 } 185 186 public XmlWrapper(String uri, String root, String[][] stringArray) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { 187 this(uri,root); 188 fillParameters(stringArray); 189 } 190 191 public XmlWrapper(){} 192 193 194 public XmlWrapper( InputSource source ) throws SAXException, IOException, ParserConfigurationException { 195 xml = javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source); 196 xpath = XPathFactory.newInstance().newXPath(); 197 values = new HashMap<String,Tuple>(); 198 } 199 200 public XmlWrapper(InputSource source, String root) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { 201 this(source); 202 xml = (Node) xpath.evaluate(root, xml, XPathConstants.NODE); 203 } 204 205 public XmlWrapper(InputSource source, String root, String[][] stringArray) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException { 206 this(source,root); 207 fillParameters(stringArray); 208 } 209 210 public String toString() { 211 // TODO: Memoize the result of this? Or not? Let's not... 212 213 String result = null; 214 try { 215 DOMImplementation impl; 216 217 switch( xml.getNodeType() ) { 218 case Node.DOCUMENT_NODE: 219 case Node.DOCUMENT_FRAGMENT_NODE: 220 impl = ((Document)xml).getImplementation(); 221 break; 222 default: 223 impl = xml.getOwnerDocument().getImplementation(); 224 } 225 226 if( impl.hasFeature("LS","3.0")) { 227 DOMImplementationLS implls = (DOMImplementationLS) 228 impl.getFeature("LS","3.0"); 229 230 LSSerializer domWriter = implls.createLSSerializer(); 231 domWriter.getDomConfig().setParameter("xml-declaration",false); 232 233 commit(); 234 result = domWriter.writeToString(xml); 235 } 236 } catch ( DOMException e) { e.printStackTrace(); 237 } catch ( LSException e) { e.printStackTrace(); 238 } catch (XPathExpressionException e) { e.printStackTrace(); 239 } catch ( ClassCastException e) { e.printStackTrace(); 240 } 241 242 return result; 243 } 244 245 246 247 @SuppressWarnings("unchecked") 248 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 249 Object result = null; 250 251 String meth = method.getName(); 252 Class[] types = method.getParameterTypes(); 253 254 if( meth.startsWith("get")) { 255 String prop = meth.substring(3); // get "g-e-t" is three characters 256 result = this.get(prop); 257 } 258 else if( meth.startsWith("is")) { 259 String prop = meth.substring(2); // is "i-s" is two characters 260 result = this.get(prop); 261 } 262 else if( meth.startsWith("set") && types.length==1 && 263 method.getReturnType()==Void.TYPE) { 264 String prop = meth.substring(3); // set "s-e-t" is three characters 265 266 if (args[0] != null && args[0] instanceof List) 267 { 268 List value = (List)args[0];// WARNING: ignore all but the first argument 269 Object oldValue = this.get(prop); 270 this.set(prop,value); 271 notifier.firePropertyChange(prop,oldValue,value); 272 } 273 } 274 else if( "toString".equals(meth)) { 275 // make sure the generic toString() of Object 276 // does not get called, but rather our implementation 277 result = this.toString(); 278 } 279 280 if( result != null && !method.getReturnType().isInstance(result) ) 281 throw new ClassCastException 282 (result.getClass().getName() + " is not a " + 283 method.getReturnType().getName()); 284 285 return result; 286 } 287 288 private PropertyChangeSupport notifier = new PropertyChangeSupport(this); 289 public void addPropertyChangeListener(PropertyChangeListener listener) { notifier.addPropertyChangeListener(listener);} 290 public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { notifier.addPropertyChangeListener(propertyName, listener); } 291 292 /** 293 * If the key points to a Tuple with multiple values in its list, this uses the first one. 294 */ 295 protected Date getDate(String key) { 296 Date result = null; 297 try { 298 List<String> val = get(key); 299 if (val != null && val.size() > 0) 300 { 301 result = ISO_8601.parse(val.get(0)); 302 if( result == null) 303 result = ISO_8601_s.parse(val.get(0)); 304 } 305 } 306 catch (ParseException e) { 307 e.printStackTrace(); 308 } 309 return result; 310 } 311 312 protected void setDate(String key, Date value) { 313 ArrayList<String> list = new ArrayList<String>(); 314 list.add(ISO_8601.format(value)); 315 set(key, list); 316 } 317 318 /** 319 * If the key points to a Tuple with multiple values in its list, this uses the first one. 320 */ 321 protected boolean getBool(String key) { 322 List<String> val = get(key); 323 if (val != null && val.size() > 0) 324 { 325 Boolean b = new Boolean(val.get(0)); 326 return b; 327 } 328 329 else 330 return false; 331 } 332 333 protected void setBool(String key, boolean value){ 334 ArrayList<String> list = new ArrayList<String>(); 335 list.add((value) ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); 336 set(key, list); 337 } 338 339 @SuppressWarnings("unchecked") 340 public static<T> T getInstanceOf(Class<T> interFace, String uri, String root, String[][] stringArray) { 341 T result = null; 342 343 try { 344 XmlWrapper poser = new XmlWrapper(uri, root, stringArray); 345 346 result = (T) Proxy.newProxyInstance 347 (XmlWrapper.class.getClassLoader(), 348 new Class[] {interFace}, 349 poser 350 ); 351 // WARNING: the result is downcast to (T) from Object, but 352 // type erasure actually generates an Object... 353 // to suppress the warning, I added the SuppressWarnings 354 355 } catch ( XPathExpressionException e) { e.printStackTrace(); 356 } catch ( IllegalArgumentException e) { e.printStackTrace(); 357 } catch ( SAXException e) { e.printStackTrace(); 358 } catch ( IOException e) { e.printStackTrace(); 359 } catch (ParserConfigurationException e) { e.printStackTrace(); 360 } 361 362 // If the XmlWrapper constructor throws an exception, return null. 363 364 return result; 365 } 366 367 @SuppressWarnings("unchecked") 368 public static<T> T getInstanceOf(Class<T> interFace, InputSource is, String root, String[][] stringArray) { 369 T result = null; 370 371 try { 372 XmlWrapper poser = new XmlWrapper(is, root, stringArray); 373 374 result = (T) Proxy.newProxyInstance 375 (XmlWrapper.class.getClassLoader(), 376 new Class[] {interFace}, 377 poser 378 ); 379 // WARNING: the result is downcast to (T) from Object, but 380 // type erasure actually generates an Object... 381 // to suppress the warning, I added the SuppressWarnings 382 383 } catch ( XPathExpressionException e) { e.printStackTrace(); 384 } catch ( IllegalArgumentException e) { e.printStackTrace(); 385 } catch ( SAXException e) { e.printStackTrace(); 386 } catch ( IOException e) { e.printStackTrace(); 387 } catch (ParserConfigurationException e) { e.printStackTrace(); 388 } 389 390 // If the XmlWrapper constructor throws an exception, return null. 391 392 return result; 393 } 394 }