Add a revisionless QName constructor
[yangtools.git] / yang / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / QName.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.Fpre
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.common;
9
10 import static org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil.getRevisionFormat;
11 import java.io.Serializable;
12 import java.net.URI;
13 import java.net.URISyntaxException;
14 import java.text.ParseException;
15 import java.util.Date;
16 import java.util.Objects;
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19 import org.opendaylight.yangtools.concepts.Immutable;
20 import org.opendaylight.yangtools.objcache.ObjectCache;
21 import org.opendaylight.yangtools.objcache.ObjectCacheFactory;
22
23 /**
24  * The QName from XML consists of local name of element and XML namespace, but
25  * for our use, we added module revision to it.
26  *
27  * In YANG context QName is full name of defined node, type, procedure or
28  * notification. QName consists of XML namespace, YANG model revision and local
29  * name of defined type. It is used to prevent name clashes between nodes with
30  * same local name, but from different schemas.
31  *
32  * <ul>
33  * <li><b>XMLNamespace</b> - {@link #getNamespace()} - the namespace assigned to the YANG module which
34  * defined element, type, procedure or notification.</li>
35  * <li><b>Revision</b> - {@link #getRevision()} - the revision of the YANG module which describes the
36  * element</li>
37  * <li><b>LocalName</b> - {@link #getLocalName()} - the YANG schema identifier which were defined for this
38  * node in the YANG module</li>
39  * </ul>
40  *
41  * QName may also have <code>prefix</code> assigned, but prefix does not
42  * affect equality and identity of two QNames and carry only information
43  * which may be useful for serializers / deserializers.
44  *
45  *
46  */
47 public final class QName implements Immutable, Serializable, Comparable<QName> {
48     private static final ObjectCache CACHE = ObjectCacheFactory.getObjectCache(QName.class);
49     private static final long serialVersionUID = 5398411242927766414L;
50
51     static final String QNAME_REVISION_DELIMITER = "?revision=";
52     static final String QNAME_LEFT_PARENTHESIS = "(";
53     static final String QNAME_RIGHT_PARENTHESIS = ")";
54
55     private static final Pattern QNAME_PATTERN_FULL = Pattern.compile("^\\((.+)\\" + QNAME_REVISION_DELIMITER
56             + "(.+)\\)(.+)$");
57     private static final Pattern QNAME_PATTERN_NO_REVISION = Pattern.compile("^\\((.+)\\)(.+)$");
58     private static final Pattern QNAME_PATTERN_NO_NAMESPACE_NO_REVISION = Pattern.compile("^(.+)$");
59     private static final char[] ILLEGAL_CHARACTERS = new char[] { '?', '(', ')', '&' };
60
61     // Mandatory
62     private final QNameModule module;
63     // Mandatory
64     private final String localName;
65
66     private QName(final QNameModule module, final String localName) {
67         this.localName = checkLocalName(localName);
68         this.module = module;
69     }
70
71     /**
72      * Look up specified QName in the global cache and return a shared reference.
73      *
74      * @param qname QName instance
75      * @return Cached instance, according to {@link ObjectCache} policy.
76      */
77     public static QName cachedReference(final QName qname) {
78         // We also want to make sure we keep the QNameModule cached
79         final QNameModule myMod = qname.getModule();
80         final QNameModule cacheMod = QNameModule.cachedReference(myMod);
81
82         final QName what;
83         if (cacheMod == myMod) {
84             what = qname;
85         } else {
86             what = QName.create(cacheMod, qname.localName);
87         }
88
89         return CACHE.getReference(what);
90     }
91
92     /**
93      * QName Constructor.
94      *
95      * @param namespace
96      *            the namespace assigned to the YANG module
97      * @param localName
98      *            YANG schema identifier
99      */
100     public QName(final URI namespace, final String localName) {
101         this(QNameModule.create(namespace, null), localName);
102     }
103
104     private static String checkLocalName(final String localName) {
105         if (localName == null) {
106             throw new IllegalArgumentException("Parameter 'localName' may not be null.");
107         }
108         if (localName.length() == 0) {
109             throw new IllegalArgumentException("Parameter 'localName' must be a non-empty string.");
110         }
111
112         for (char c : ILLEGAL_CHARACTERS) {
113             if (localName.indexOf(c) != -1) {
114                 throw new IllegalArgumentException(String.format(
115                         "Parameter 'localName':'%s' contains illegal character '%s'", localName, c));
116             }
117         }
118         return localName;
119     }
120
121     public static QName create(final String input) {
122         Matcher matcher = QNAME_PATTERN_FULL.matcher(input);
123         if (matcher.matches()) {
124             String namespace = matcher.group(1);
125             String revision = matcher.group(2);
126             String localName = matcher.group(3);
127             return create(namespace, revision, localName);
128         }
129         matcher = QNAME_PATTERN_NO_REVISION.matcher(input);
130         if (matcher.matches()) {
131             URI namespace = URI.create(matcher.group(1));
132             String localName = matcher.group(2);
133             return new QName(namespace, localName);
134         }
135         matcher = QNAME_PATTERN_NO_NAMESPACE_NO_REVISION.matcher(input);
136         if (matcher.matches()) {
137             String localName = matcher.group(1);
138             return new QName((URI) null, localName);
139         }
140         throw new IllegalArgumentException("Invalid input:" + input);
141     }
142
143     /**
144      * Get the module component of the QName.
145      *
146      * @return Module component
147      */
148     public QNameModule getModule() {
149         return module;
150     }
151
152     /**
153      * Returns XMLNamespace assigned to the YANG module.
154      *
155      * @return XMLNamespace assigned to the YANG module.
156      */
157     public URI getNamespace() {
158         return module.getNamespace();
159     }
160
161     /**
162      * Returns YANG schema identifier which were defined for this node in the
163      * YANG module
164      *
165      * @return YANG schema identifier which were defined for this node in the
166      *         YANG module
167      */
168     public String getLocalName() {
169         return localName;
170     }
171
172     /**
173      * Returns revision of the YANG module if the module has defined revision,
174      * otherwise returns <code>null</code>
175      *
176      * @return revision of the YANG module if the module has defined revision,
177      *         otherwise returns <code>null</code>
178      */
179     public Date getRevision() {
180         return module.getRevision();
181     }
182
183     @Override
184     public int hashCode() {
185         final int prime = 31;
186         int result = 1;
187         result = prime * result + ((localName == null) ? 0 : localName.hashCode());
188         result = prime * result + module.hashCode();
189         return result;
190     }
191
192     /**
193      *
194      * Compares the specified object with this list for equality.  Returns
195      * <tt>true</tt> if and only if the specified object is also instance of
196      * {@link QName} and its {@link #getLocalName()}, {@link #getNamespace()} and
197      * {@link #getRevision()} are equals to same properties of this instance.
198      *
199      * @param obj the object to be compared for equality with this QName
200      * @return <tt>true</tt> if the specified object is equal to this QName
201      *
202      */
203     @Override
204     public boolean equals(final Object obj) {
205         if (this == obj) {
206             return true;
207         }
208         if (!(obj instanceof QName)) {
209             return false;
210         }
211         final QName other = (QName) obj;
212         if (localName == null) {
213             if (other.localName != null) {
214                 return false;
215             }
216         } else if (!localName.equals(other.localName)) {
217             return false;
218         }
219         return module.equals(other.module);
220     }
221
222     public static QName create(final QName base, final String localName) {
223         return create(base.getModule(), localName);
224     }
225
226     /**
227      * Creates new QName.
228      *
229      * @param qnameModule
230      *            Namespace and revision enclosed as a QNameModule
231      * @param localName
232      *            Local name part of QName. MUST NOT BE null.
233      * @return Instance of QName
234      */
235     public static QName create(final QNameModule qnameModule, final String localName) {
236         if (qnameModule == null) {
237             throw new NullPointerException("module may not be null");
238         }
239         return new QName(qnameModule, localName);
240     }
241
242     /**
243      * Creates new QName.
244      *
245      * @param namespace
246      *            Namespace of QName or null if namespace is undefined.
247      * @param revision
248      *            Revision of namespace or null if revision is unspecified.
249      * @param localName
250      *            Local name part of QName. MUST NOT BE null.
251      * @return Instance of QName
252      */
253     public static QName create(final URI namespace, final Date revision, final String localName) {
254         return create(QNameModule.create(namespace, revision), localName);
255     }
256
257     /**
258      *
259      * Creates new QName.
260      *
261      * @param namespace
262      *            Namespace of QName, MUST NOT BE Null.
263      * @param revision
264      *            Revision of namespace / YANG module. MUST NOT BE null, MUST BE
265      *            in format <code>YYYY-mm-dd</code>.
266      * @param localName
267      *            Local name part of QName. MUST NOT BE null.
268      * @return
269      * @throws NullPointerException
270      *             If any of parameters is null.
271      * @throws IllegalArgumentException
272      *             If <code>namespace</code> is not valid URI or
273      *             <code>revision</code> is not according to format
274      *             <code>YYYY-mm-dd</code>.
275      */
276     public static QName create(final String namespace, final String revision, final String localName)
277             throws IllegalArgumentException {
278         final URI namespaceUri = parseNamespace(namespace);
279         final Date revisionDate = parseRevision(revision);
280         return create(namespaceUri, revisionDate, localName);
281     }
282
283     private static URI parseNamespace(final String namespace) {
284         try {
285             return new URI(namespace);
286         } catch (URISyntaxException ue) {
287             throw new IllegalArgumentException(String.format("Namespace '%s' is not a valid URI", namespace), ue);
288         }
289     }
290
291     /**
292      * Creates new QName.
293      *
294      * @param namespace
295      *            Namespace of QName, MUST NOT BE Null.
296      * @param localName
297      *            Local name part of QName. MUST NOT BE null.
298      * @return
299      * @throws NullPointerException
300      *             If any of parameters is null.
301      * @throws IllegalArgumentException
302      *             If <code>namespace</code> is not valid URI.
303      */
304     public static QName create(final String namespace, final String localName) throws IllegalArgumentException {
305         return create(parseNamespace(namespace), null, localName);
306     }
307
308     @Override
309     public String toString() {
310         StringBuilder sb = new StringBuilder();
311         if (getNamespace() != null) {
312             sb.append(QNAME_LEFT_PARENTHESIS + getNamespace());
313
314             if (getFormattedRevision() != null) {
315                 sb.append(QNAME_REVISION_DELIMITER + getFormattedRevision());
316             }
317             sb.append(QNAME_RIGHT_PARENTHESIS);
318         }
319         sb.append(localName);
320         return sb.toString();
321     }
322
323     /**
324      * Return string representation of revision in format
325      * <code>YYYY-mm-dd</code>
326      *
327      * YANG Specification defines format for <code>revision</code> as
328      * YYYY-mm-dd. This format for revision is reused accross multiple places
329      * such as capabilities URI, YANG modules, etc.
330      *
331      * @return String representation of revision or null, if revision is not
332      *         set.
333      */
334     public String getFormattedRevision() {
335         return module.getFormattedRevision();
336     }
337
338     /**
339      * Creates copy of this with revision and prefix unset.
340      *
341      * @return copy of this QName with revision and prefix unset.
342      */
343     public QName withoutRevision() {
344         return create(getNamespace(), null, localName);
345     }
346
347     public static Date parseRevision(final String formatedDate) {
348         try {
349             return getRevisionFormat().parse(formatedDate);
350         } catch (ParseException | RuntimeException e) {
351             throw new IllegalArgumentException(
352                     String.format("Revision '%s'is not in a supported format", formatedDate), e);
353         }
354     }
355
356     /**
357      * Formats {@link Date} representing revision to format
358      * <code>YYYY-mm-dd</code>
359      *
360      * YANG Specification defines format for <code>revision</code> as
361      * YYYY-mm-dd. This format for revision is reused accross multiple places
362      * such as capabilities URI, YANG modules, etc.
363      *
364      * @param revision
365      *            Date object to format or null
366      * @return String representation or null if the input was null.
367      */
368     public static String formattedRevision(final Date revision) {
369         if (revision == null) {
370             return null;
371         }
372         return getRevisionFormat().format(revision);
373     }
374
375     /**
376      *
377      * Compares this QName to other, without comparing revision.
378      *
379      * Compares instance of this to other instance of QName and returns true if
380      * both instances have equal <code>localName</code> ({@link #getLocalName()}
381      * ) and <code>namespace</code> ({@link #getNamespace()}).
382      *
383      * @param other
384      *            Other QName. Must not be null.
385      * @return true if this instance and other have equals localName and
386      *         namespace.
387      * @throws NullPointerException
388      *             if <code>other</code> is null.
389      */
390     public boolean isEqualWithoutRevision(final QName other) {
391         return localName.equals(other.getLocalName()) && Objects.equals(getNamespace(), other.getNamespace());
392     }
393
394     @Override
395     public int compareTo(final QName other) {
396         // compare mandatory localName parameter
397         int result = localName.compareTo(other.localName);
398         if (result != 0) {
399             return result;
400         }
401
402         // compare nullable namespace parameter
403         if (getNamespace() == null) {
404             if (other.getNamespace() != null) {
405                 return -1;
406             }
407         } else {
408             if (other.getNamespace() == null) {
409                 return 1;
410             }
411             result = getNamespace().compareTo(other.getNamespace());
412             if (result != 0) {
413                 return result;
414             }
415         }
416
417         // compare nullable revision parameter
418         if (getRevision() == null) {
419             if (other.getRevision() != null) {
420                 return -1;
421             }
422         } else {
423             if (other.getRevision() == null) {
424                 return 1;
425             }
426             result = getRevision().compareTo(other.getRevision());
427             if (result != 0) {
428                 return result;
429             }
430         }
431
432         return result;
433     }
434
435 }