View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.support.lastchange;
17  
18  import static org.fourthline.cling.model.XMLUtil.appendNewElement;
19  
20  import java.io.InputStream;
21  import java.io.StringReader;
22  import java.lang.reflect.Constructor;
23  import java.util.Collections;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.logging.Level;
27  import java.util.logging.Logger;
28  
29  import javax.xml.parsers.DocumentBuilderFactory;
30  
31  import org.fourthline.cling.model.XMLUtil;
32  import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
33  import org.fourthline.cling.support.shared.AbstractMap;
34  import org.seamless.util.io.IO;
35  import org.seamless.util.Exceptions;
36  import org.seamless.xml.DOMParser;
37  import org.seamless.xml.SAXParser;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.Element;
40  import org.xml.sax.Attributes;
41  import org.xml.sax.InputSource;
42  import org.xml.sax.SAXException;
43  
44  /**
45   * Reads and writes the "LastChange" XML content.
46   * <p>
47   * Validates against a schema if the {@link #getSchemaSources()} method
48   * doesn't return <code>null</code>.
49   * </p>
50   * <p>
51   * Note: This is broken on most devices and with most services out in the wild. In fact,
52   * you might want to use polling the service with actions, to get its status, instead of
53   * GENA. Polling can be expensive on low-power control points, however.
54   * </p>
55   *
56   * @author Christian Bauer
57   */
58  public abstract class LastChangeParser extends SAXParser {
59  
60      final private static Logger log = Logger.getLogger(LastChangeParser.class.getName());
61  
62      public enum CONSTANTS {
63          Event,
64          InstanceID,
65          val;
66  
67          public boolean equals(String s) {
68              return this.name().equals(s);
69          }
70      }
71  
72      abstract protected String getNamespace();
73  
74      protected Set<Class<? extends EventedValue>> getEventedVariables() {
75          return Collections.EMPTY_SET;
76      }
77  
78      protected EventedValue createValue(String name, Map.Entry<String, String>[] attributes) throws Exception {
79          for (Class<? extends EventedValue> evType : getEventedVariables()) {
80              if (evType.getSimpleName().equals(name)) {
81                  Constructor<? extends EventedValue> ctor = evType.getConstructor(Map.Entry[].class);
82                  return ctor.newInstance(new Object[]{attributes});
83              }
84          }
85          return null;
86      }
87  
88      /**
89       * Uses the current thread's context classloader to read and unmarshall the given resource.
90       *
91       * @param resource The resource on the classpath.
92       * @return The unmarshalled Event model.
93       * @throws Exception
94       */
95      public Event parseResource(String resource) throws Exception {
96          InputStream is = null;
97          try {
98              is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
99              return parse(IO.readLines(is));
100         } finally {
101             if (is != null) is.close();
102         }
103     }
104 
105     public Event parse(String xml) throws Exception {
106 
107         if (xml == null || xml.length() == 0) {
108             throw new RuntimeException("Null or empty XML");
109         }
110 
111         Event event = new Event();
112         new RootHandler(event, this);
113 
114         if (log.isLoggable(Level.FINE)) {
115             log.fine("Parsing 'LastChange' event XML content");
116             log.fine("===================================== 'LastChange' BEGIN ============================================");
117             log.fine(xml);
118             log.fine("====================================== 'LastChange' END  ============================================");
119         }
120         parse(new InputSource(new StringReader(xml)));
121 
122         log.fine("Parsed event with instances IDs: " + event.getInstanceIDs().size());
123         if (log.isLoggable(Level.FINEST)) {
124             for (InstanceID instanceID : event.getInstanceIDs()) {
125                 log.finest("InstanceID '" + instanceID.getId() + "' has values: " + instanceID.getValues().size());
126                 for (EventedValue eventedValue : instanceID.getValues()) {
127                     log.finest(eventedValue.getName() + " => " + eventedValue.getValue());
128                 }
129             }
130         }
131 
132         return event;
133     }
134 
135     class RootHandler extends SAXParser.Handler<Event> {
136 
137         RootHandler(Event instance, SAXParser parser) {
138             super(instance, parser);
139         }
140 
141         RootHandler(Event instance) {
142             super(instance);
143         }
144 
145         @Override
146         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
147             super.startElement(uri, localName, qName, attributes);
148             if (CONSTANTS.InstanceID.equals(localName)) {
149                 String valAttr = attributes.getValue(CONSTANTS.val.name());
150                 if (valAttr != null) {
151                     InstanceID instanceID = new InstanceID(new UnsignedIntegerFourBytes(valAttr));
152                     getInstance().getInstanceIDs().add(instanceID);
153                     new InstanceIDHandler(instanceID, this);
154                 }
155             }
156         }
157     }
158 
159     class InstanceIDHandler extends SAXParser.Handler<InstanceID> {
160 
161         InstanceIDHandler(InstanceID instance, SAXParser.Handler parent) {
162             super(instance, parent);
163         }
164 
165         @Override
166         public void startElement(String uri, String localName, String qName, final Attributes attributes) throws SAXException {
167             super.startElement(uri, localName, qName, attributes);
168             Map.Entry[] attributeMap = new Map.Entry[attributes.getLength()];
169             for (int i = 0; i < attributeMap.length; i++) {
170                 attributeMap[i] =
171                         new AbstractMap.SimpleEntry<>(
172                                 attributes.getLocalName(i),
173                                 attributes.getValue(i)
174                         );
175             }
176             try {
177                 EventedValue esv = createValue(localName, attributeMap);
178                 if (esv != null)
179                     getInstance().getValues().add(esv);
180             } catch (Exception ex) {
181                 // Don't exit, just log a warning
182                 log.warning("Error reading event XML, ignoring value: " + Exceptions.unwrap(ex));
183             }
184         }
185 
186         @Override
187         protected boolean isLastElement(String uri, String localName, String qName) {
188             return CONSTANTS.InstanceID.equals(localName);
189         }
190     }
191 
192     public String generate(Event event) throws Exception {
193         return XMLUtil.documentToFragmentString(buildDOM(event));
194     }
195 
196     protected Document buildDOM(Event event) throws Exception {
197 
198         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
199         factory.setNamespaceAware(true);
200 
201         Document d = factory.newDocumentBuilder().newDocument();
202         generateRoot(event, d);
203 
204         return d;
205     }
206 
207     protected void generateRoot(Event event, Document descriptor) {
208         Element eventElement = descriptor.createElementNS(getNamespace(), CONSTANTS.Event.name());
209         descriptor.appendChild(eventElement);
210         generateInstanceIDs(event, descriptor, eventElement);
211     }
212 
213     protected void generateInstanceIDs(Event event, Document descriptor, Element rootElement) {
214         for (InstanceID instanceID : event.getInstanceIDs()) {
215             if (instanceID.getId() == null) continue;
216             Element instanceIDElement = appendNewElement(descriptor, rootElement, CONSTANTS.InstanceID.name());
217             instanceIDElement.setAttribute(CONSTANTS.val.name(), instanceID.getId().toString());
218 
219             for (EventedValue eventedValue : instanceID.getValues()) {
220                 generateEventedValue(eventedValue, descriptor, instanceIDElement);
221             }
222         }
223     }
224 
225     protected void generateEventedValue(EventedValue eventedValue, Document descriptor, Element parentElement) {
226         String name = eventedValue.getName();
227         Map.Entry<String, String>[] attributes = eventedValue.getAttributes();
228         if (attributes != null && attributes.length > 0) {
229             Element evElement = appendNewElement(descriptor, parentElement, name);
230             for (Map.Entry<String, String> attr : attributes) {
231                 evElement.setAttribute(attr.getKey(), DOMParser.escape(attr.getValue()));
232             }
233         }
234     }
235 
236 }