View Javadoc

1   /* This file is part of COPAL (COntext Provisioning for All).
2    *
3    * COPAL is a part of SM4All (Smart hoMes for All) project.
4    *
5    * COPAL is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU Lesser General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * COPAL is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public License
16   * along with COPAL. If not, see <http://www.gnu.org/licenses/>.
17   */
18  package at.ac.tuwien.infosys.sm4all.copal.api.event;
19  
20  import java.io.IOException;
21  import java.net.URI;
22  import java.net.URL;
23  import java.text.MessageFormat;
24  import javax.xml.XMLConstants;
25  import javax.xml.parsers.DocumentBuilder;
26  import javax.xml.transform.dom.DOMSource;
27  import javax.xml.validation.Schema;
28  import javax.xml.validation.SchemaFactory;
29  import javax.xml.validation.Validator;
30  import org.apache.log4j.Level;
31  import org.apache.log4j.Logger;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.Element;
34  import org.xml.sax.SAXException;
35  import at.ac.tuwien.infosys.sm4all.copal.api.listener.ContextListener;
36  import at.ac.tuwien.infosys.sm4all.copal.api.publisher.ContextPublisher;
37  import static at.ac.tuwien.infosys.sm4all.copal.api.xml.Constants.XML_NAMESPACE_PREFIX;
38  import static at.ac.tuwien.infosys.sm4all.copal.api.xml.Constants.XML_NAMESPACE_URI;
39  
40  /**
41   * Class which defines type for each {@link XMLContextEvent}, i.e. each
42   * {@link XMLContextEvent} must have a corresponding {@link XMLContextEventType}
43   * associated with it. The name of the {@link XMLContextEventType} is also the
44   * name of each {@link XMLContextEvent} associated with this type and is a
45   * globally unique name on which {@link ContextPublisher}s and
46   * {@link ContextListener}s have to agree on and use it to publish and listen on
47   * events respectively. This class is not thread safe.
48   * 
49   * @author sanjin
50   */
51  public class XMLContextEventType extends ContextEventType {
52  
53      private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
54      private static final Logger LOGGER = Logger.getLogger(XMLContextEventType.class);
55  
56      private final String rootElementName;
57      private URI namespaceURI;
58      private URL schemaURL;
59      private Validator validator;
60  
61      /**
62       * Create instance of {@link XMLContextEventType}. The <code>name</code>
63       * must be globally unique name of the event. If <code>name</code> and
64       * <code>rootElementName</code> have same value use the
65       * {@link #XMLContextEventType(String)} constructor instead.
66       * 
67       * @param name the globally unique name of the {@link XMLContextEventType}.
68       * @param rootElementName the local (unqualified) name of event's
69       *        {@link Document} root element.
70       * @throws NullPointerException if specified name or root element name is
71       *         <code>null</code>.
72       * @throws IllegalArgumentException if specified name or root element name
73       *         is an empty or blank string.
74       */
75      public XMLContextEventType(final String name, final String rootElementName) {
76          super(name);
77  
78          if (null == rootElementName) {
79              throw new NullPointerException("Root element name cannot be null.");
80          }
81          if (rootElementName.trim().isEmpty()) {
82              throw new IllegalArgumentException(
83                      "Root element name cannot be empty or blank.");
84          }
85  
86          this.rootElementName = rootElementName;
87      }
88  
89      /**
90       * Create instance of {@link XMLContextEventType}. The <code>name</code>
91       * must be globally unique name. If <code>name</code> and
92       * <code>rootElementName</code> have different values use the
93       * {@link #XMLContextEventType(String, String)} constructor instead.
94       * 
95       * @param name the globally unique name of the event and the local
96       *        (unqualified) name of XML document root element.
97       * @throws NullPointerException if specified name is <code>null</code>.
98       * @throws IllegalArgumentException if specified name is an empty or blank
99       *         string.
100      */
101     public XMLContextEventType(final String name) {
102         this(name, name);
103     }
104 
105     /**
106      * @return the local (unqualified) name of XML document root element.
107      */
108     public String getRootElementName() {
109         return this.rootElementName;
110     }
111 
112     /**
113      * @return the namespace {@link URI} used by event for its XML elements.
114      */
115     public URI getNamespaceURI() {
116         return this.namespaceURI;
117     }
118 
119     /**
120      * Sets the value of the namespace {@link URI} used by event for its XML
121      * elements.
122      * 
123      * @param namespaceURI the new namespace {@link URI}.
124      */
125     public void setNamespaceURI(final URI namespaceURI) {
126         this.namespaceURI = namespaceURI;
127     }
128 
129     /**
130      * @return if this {@link ContextEventType} has the namespace defined.
131      */
132     public boolean hasNamespace() {
133         return null != this.namespaceURI;
134     }
135 
136     /**
137      * @return the URL where XML Schema can be found that defines the XML
138      *         document structure of this {@link ContextEventType}.
139      */
140     public URL getSchemaURL() {
141         return this.schemaURL;
142     }
143 
144     /**
145      * Sets the XML Schema URL that defines the XML document structure of this
146      * {@link ContextEventType}.
147      * 
148      * @param schemaURL the new XML Schema URL.
149      */
150     public void setSchemaURL(final URL schemaURL) {
151         this.schemaURL = schemaURL;
152         this.validator = null;
153     }
154 
155     /**
156      * @return if this {@link ContextEventType} has the schema defined.
157      */
158     public boolean hasSchema() {
159         return null != this.schemaURL;
160     }
161 
162     /**
163      * Create a {@link Document} for this {@link ContextEventType} using
164      * specified {@link DocumentBuilder} with root element having name equal to
165      * {@link #getRootElementName()} and default namespace set to
166      * {@link #getNamespaceURI()} if there is one.
167      * 
168      * @param builder the {@link DocumentBuilder} used for creating
169      *        {@link Document}.
170      * @return the {@link Document} that can be used as an event for this
171      *         {@link ContextEventType}.
172      * @throws NullPointerException id specified {@link DocumentBuilder} is
173      *         <code>null</code>.
174      */
175     public Document createEvent(final DocumentBuilder builder) {
176         if (null == builder) {
177             throw new NullPointerException("Document builder cannot be null.");
178         }
179 
180         final Document result = builder.newDocument();
181 
182         final Element rootElement;
183         if (hasNamespace()) {
184             final String namespace = this.namespaceURI.toString();
185             rootElement = result.createElementNS(namespace,
186                     this.rootElementName);
187             rootElement.setAttributeNS(XML_NAMESPACE_URI, XML_NAMESPACE_PREFIX,
188                     namespace);
189         } else {
190             rootElement = result.createElementNS(null, this.rootElementName);
191         }
192 
193         result.appendChild(rootElement);
194 
195         return result;
196     }
197 
198     /**
199      * Checks if specified {@link Document} is valid for this
200      * {@link ContextEventType}.
201      * 
202      * @param document the {@link Document}.
203      * @throws MalformedDocumentException if specified {@link Document} is
204      *         malformed for this {@link ContextEventType}.
205      */
206     public void validate(final Document document)
207             throws MalformedDocumentException {
208         if (null == document) {
209             throw new NullPointerException("XML DOM document cannot be null.");
210         }
211 
212         if ((null != this.schemaURL) && (null == this.validator)) {
213             try {
214                 final Schema schema = SCHEMA_FACTORY.newSchema(this.schemaURL);
215                 this.validator = schema.newValidator();
216             } catch (final SAXException ex) {
217                 if (LOGGER.isEnabledFor(Level.WARN)) {
218                     LOGGER.warn(
219                             MessageFormat.format(
220                                     "Could not read XML Schema from ''{0}'' URL! URL will remain unchanged.",
221                                     this.schemaURL), ex);
222                 }
223             }
224         }
225 
226         if (null != this.validator) {
227             try {
228                 this.validator.validate(new DOMSource(document));
229             } catch (final SAXException ex) {
230                 throw new MalformedDocumentException(
231                         "XML Schema validation failed.", ex);
232             } catch (final IOException ex) {
233                 throw new MalformedDocumentException(
234                         "Could not read XML DOM document.", ex);
235             }
236         } else {
237             final Element rootElement = document.getDocumentElement();
238             if ((null == rootElement)
239                     || (!this.rootElementName.equals(rootElement.getLocalName()))) {
240                 throw new MalformedDocumentException(MessageFormat.format(
241                         "Document is missing root element ''{0}''.",
242                         this.rootElementName));
243             }
244 
245             if (hasNamespace()) {
246                 if (!this.namespaceURI.toString().equals(
247                         rootElement.getNamespaceURI())) {
248                     throw new MalformedDocumentException(MessageFormat.format(
249                             "Root element is not in ''{0}'' namespace.",
250                             this.namespaceURI));
251                 }
252             } else if (null != rootElement.getNamespaceURI()) {
253                 throw new MalformedDocumentException(
254                         "Root element is not in no namespace.");
255             }
256         }
257     }
258 }