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