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    }