Bug 4662: Introduce a SemanticVersion concept - pre-linkage phase
[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 org.opendaylight.yangtools.concepts.Immutable;
24
25 /**
26  * The QName from XML consists of local name of element and XML namespace, but
27  * for our use, we added module revision to it.
28  *
29  * In YANG context QName is full name of defined node, type, procedure or
30  * notification. QName consists of XML namespace, YANG model revision and local
31  * name of defined type. It is used to prevent name clashes between nodes with
32  * same local name, but from different schemas.
33  *
34  * <ul>
35  * <li><b>XMLNamespace</b> - {@link #getNamespace()} - the namespace assigned to the YANG module which
36  * defined element, type, procedure or notification.</li>
37  * <li><b>Revision</b> - {@link #getRevision()} - the revision of the YANG module which describes the
38  * element</li>
39  * <li><b>LocalName</b> - {@link #getLocalName()} - the YANG schema identifier which were defined for this
40  * node in the YANG module</li>
41  * </ul>
42  *
43  * QName may also have <code>prefix</code> assigned, but prefix does not
44  * affect equality and identity of two QNames and carry only information
45  * which may be useful for serializers / deserializers.
46  *
47  *
48  */
49 public final class QName implements Immutable, Serializable, Comparable<QName> {
50     private static final Interner<QName> INTERNER = Interners.newWeakInterner();
51     private static final long serialVersionUID = 5398411242927766414L;
52
53     static final String QNAME_REVISION_DELIMITER = "?revision=";
54     static final String QNAME_LEFT_PARENTHESIS = "(";
55     static final String QNAME_RIGHT_PARENTHESIS = ")";
56
57     private static final Pattern QNAME_PATTERN_FULL = Pattern.compile("^\\((.+)\\" + QNAME_REVISION_DELIMITER
58             + "(.+)\\)(.+)$");
59     private static final Pattern QNAME_PATTERN_NO_REVISION = Pattern.compile("^\\((.+)\\)(.+)$");
60     private static final Pattern QNAME_PATTERN_NO_NAMESPACE_NO_REVISION = Pattern.compile("^(.+)$");
61     private static final char[] ILLEGAL_CHARACTERS = new char[] { '?', '(', ')', '&' };
62
63     // Mandatory
64     private final QNameModule module;
65     // Mandatory
66     private final String localName;
67
68     private QName(final QNameModule module, final String localName) {
69         this.localName = checkLocalName(localName);
70         this.module = module;
71     }
72
73     /**
74      * Look up specified QName in the global cache and return a shared reference.
75      *
76      * @param qname QName instance
77      * @return Cached instance, according to {@link org.opendaylight.yangtools.objcache.ObjectCache} policy.
78      *
79      * @deprecated Use {@link #intern()} instead.
80      */
81     @Deprecated
82     public static QName cachedReference(final QName qname) {
83         return qname.intern();
84     }
85
86     /**
87      * QName Constructor.
88      *
89      * @param namespace
90      *            the namespace assigned to the YANG module
91      * @param localName
92      *            YANG schema identifier
93      */
94     public QName(final URI namespace, final String localName) {
95         this(QNameModule.create(namespace, null), localName);
96     }
97
98     private static String checkLocalName(final String localName) {
99         if (localName == null) {
100             throw new IllegalArgumentException("Parameter 'localName' may not be null.");
101         }
102         if (localName.length() == 0) {
103             throw new IllegalArgumentException("Parameter 'localName' must be a non-empty string.");
104         }
105
106         for (final char c : ILLEGAL_CHARACTERS) {
107             if (localName.indexOf(c) != -1) {
108                 throw new IllegalArgumentException(String.format(
109                         "Parameter 'localName':'%s' contains illegal character '%s'", localName, c));
110             }
111         }
112         return localName;
113     }
114
115     public static QName create(final String input) {
116         Matcher matcher = QNAME_PATTERN_FULL.matcher(input);
117         if (matcher.matches()) {
118             final String namespace = matcher.group(1);
119             final String revision = matcher.group(2);
120             final String localName = matcher.group(3);
121             return create(namespace, revision, localName);
122         }
123         matcher = QNAME_PATTERN_NO_REVISION.matcher(input);
124         if (matcher.matches()) {
125             final URI namespace = URI.create(matcher.group(1));
126             final String localName = matcher.group(2);
127             return new QName(namespace, localName);
128         }
129         matcher = QNAME_PATTERN_NO_NAMESPACE_NO_REVISION.matcher(input);
130         if (matcher.matches()) {
131             final String localName = matcher.group(1);
132             return new QName((URI) null, localName);
133         }
134         throw new IllegalArgumentException("Invalid input:" + input);
135     }
136
137     /**
138      * Get the module component of the QName.
139      *
140      * @return Module component
141      */
142     public QNameModule getModule() {
143         return module;
144     }
145
146     /**
147      * Returns XMLNamespace assigned to the YANG module.
148      *
149      * @return XMLNamespace assigned to the YANG module.
150      */
151     public URI getNamespace() {
152         return module.getNamespace();
153     }
154
155     /**
156      * Returns YANG schema identifier which were defined for this node in the
157      * YANG module
158      *
159      * @return YANG schema identifier which were defined for this node in the
160      *         YANG module
161      */
162     public String getLocalName() {
163         return localName;
164     }
165
166     /**
167      * Returns revision of the YANG module if the module has defined revision,
168      * otherwise returns <code>null</code>
169      *
170      * @return revision of the YANG module if the module has defined revision,
171      *         otherwise returns <code>null</code>
172      */
173     public Date getRevision() {
174         return module.getRevision();
175     }
176
177     /**
178      * Return an interned reference to a equivalent QName.
179      *
180      * @return Interned reference, or this object if it was interned.
181      */
182     public QName intern() {
183         // We also want to make sure we keep the QNameModule cached
184         final QNameModule cacheMod = module.intern();
185
186         // Identity comparison is here on purpose, as we are deciding whether to potentially store 'qname' into the
187         // interner. It is important that it does not hold user-supplied reference (such a String instance from
188         // parsing of an XML document).
189         final QName template = cacheMod == module ? this : QName.create(cacheMod, localName);
190
191         return INTERNER.intern(template);
192     }
193
194     @Override
195     public int hashCode() {
196         final int prime = 31;
197         int result = 1;
198         result = prime * result + Objects.hashCode(localName);
199         result = prime * result + module.hashCode();
200         return result;
201     }
202
203     /**
204      *
205      * Compares the specified object with this list for equality.  Returns
206      * <tt>true</tt> if and only if the specified object is also instance of
207      * {@link QName} and its {@link #getLocalName()}, {@link #getNamespace()} and
208      * {@link #getRevision()} are equals to same properties of this instance.
209      *
210      * @param obj the object to be compared for equality with this QName
211      * @return <tt>true</tt> if the specified object is equal to this QName
212      *
213      */
214     @Override
215     public boolean equals(final Object obj) {
216         if (this == obj) {
217             return true;
218         }
219         if (!(obj instanceof QName)) {
220             return false;
221         }
222         final QName other = (QName) obj;
223         return Objects.equals(localName, other.localName) && module.equals(other.module);
224     }
225
226     public static QName create(final QName base, final String localName) {
227         return create(base.getModule(), localName);
228     }
229
230     /**
231      * Creates new QName.
232      *
233      * @param qnameModule
234      *            Namespace and revision enclosed as a QNameModule
235      * @param localName
236      *            Local name part of QName. MUST NOT BE null.
237      * @return Instance of QName
238      */
239     public static QName create(final QNameModule qnameModule, final String localName) {
240         return new QName(Preconditions.checkNotNull(qnameModule,"module may not be null"), localName);
241     }
242
243     /**
244      * Creates new QName.
245      *
246      * @param namespace
247      *            Namespace of QName or null if namespace is undefined.
248      * @param revision
249      *            Revision of namespace or null if revision is unspecified.
250      * @param localName
251      *            Local name part of QName. MUST NOT BE null.
252      * @return Instance of QName
253      */
254     public static QName create(final URI namespace, final Date revision, final String localName) {
255         return create(QNameModule.create(namespace, revision), localName);
256     }
257
258     /**
259      *
260      * Creates new QName.
261      *
262      * @param namespace
263      *            Namespace of QName, MUST NOT BE Null.
264      * @param revision
265      *            Revision of namespace / YANG module. MUST NOT BE null, MUST BE
266      *            in format <code>YYYY-mm-dd</code>.
267      * @param localName
268      *            Local name part of QName. MUST NOT BE null.
269      * @return
270      * @throws NullPointerException
271      *             If any of parameters is null.
272      * @throws IllegalArgumentException
273      *             If <code>namespace</code> is not valid URI or
274      *             <code>revision</code> is not according to format
275      *             <code>YYYY-mm-dd</code>.
276      */
277     public static QName create(final String namespace, final String revision, final String localName) {
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 (final 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) {
305         return create(parseNamespace(namespace), null, localName);
306     }
307
308     @Override
309     public String toString() {
310         final StringBuilder sb = new StringBuilder();
311         if (getNamespace() != null) {
312             sb.append(QNAME_LEFT_PARENTHESIS).append(getNamespace());
313
314             if (getFormattedRevision() != null) {
315                 sb.append(QNAME_REVISION_DELIMITER).append(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 }