Add QNameAwareData{Input,Output}
[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 java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.Beta;
13 import com.google.common.collect.Interner;
14 import com.google.common.collect.Interners;
15 import java.io.DataInput;
16 import java.io.DataOutput;
17 import java.io.IOException;
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24 import org.checkerframework.checker.regex.qual.Regex;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.Nullable;
27
28 /**
29  * The QName from XML consists of local name of element and XML namespace, but for our use, we added module revision to
30  * it.
31  *
32  * <p>
33  * In YANG context QName is full name of defined node, type, procedure or notification. QName consists of XML namespace,
34  * YANG model revision and local name of defined type. It is used to prevent name clashes between nodes with same local
35  * name, but from different schemas.
36  *
37  * <p>
38  * The local name must conform to <a href="https://tools.ietf.org/html/rfc7950#section-6.2">RFC7950 Section 6.2</a>.
39  *
40  * <ul>
41  * <li><b>XMLNamespace</b> - {@link #getNamespace()} - the namespace assigned to the YANG module which
42  * defined element, type, procedure or notification.</li>
43  * <li><b>Revision</b> - {@link #getRevision()} - the revision of the YANG module which describes the
44  * element</li>
45  * <li><b>LocalName</b> - {@link #getLocalName()} - the YANG schema identifier which were defined for this
46  * node in the YANG module</li>
47  * </ul>
48  */
49 public final class QName extends AbstractQName implements Comparable<QName> {
50     /**
51      * A {@link DataInput} which has an understanding of {@link QName}'s semantics.
52      */
53     @Beta
54     public interface QNameAwareDataInput extends DataInput {
55         /**
56          * Read a {@link QName} from the stream.
57          *
58          * @return A QName
59          * @throws IOException if an I/O error occurs.
60          */
61         @NonNull QName readQName() throws IOException;
62     }
63
64     @Beta
65     public interface QNameAwareDataOutput extends DataOutput {
66         /**
67          * Write a {@link QName} into the stream.
68          *
69          * @param qname A QName
70          * @throws  IOException if an I/O error occurs.
71          */
72         void writeQName(@NonNull QName qname) throws IOException;
73     }
74
75     private static final Interner<QName> INTERNER = Interners.newWeakInterner();
76     // Note: 5398411242927766414L is used for versions < 3.0.0 without writeReplace
77     private static final long serialVersionUID = 1L;
78
79     static final String QNAME_REVISION_DELIMITER = "?revision=";
80     static final String QNAME_LEFT_PARENTHESIS = "(";
81     static final String QNAME_RIGHT_PARENTHESIS = ")";
82
83     @Regex
84     private static final String QNAME_STRING_FULL = "^\\((.+)\\?revision=(.+)\\)(.+)$";
85     private static final Pattern QNAME_PATTERN_FULL = Pattern.compile(QNAME_STRING_FULL);
86
87     @Regex
88     private static final String QNAME_STRING_NO_REVISION = "^\\((.+)\\)(.+)$";
89     private static final Pattern QNAME_PATTERN_NO_REVISION = Pattern.compile(QNAME_STRING_NO_REVISION);
90
91     private final @NonNull QNameModule module;
92     private transient int hash = 0;
93
94     QName(final QNameModule module, final @NonNull String localName) {
95         super(localName);
96         this.module = requireNonNull(module);
97     }
98
99     /**
100      * QName Constructor.
101      *
102      * @param namespace
103      *            the namespace assigned to the YANG module
104      * @param localName
105      *            YANG schema identifier
106      */
107     private QName(final URI namespace, final String localName) {
108         this(QNameModule.create(namespace), checkLocalName(localName));
109     }
110
111     public static @NonNull QName create(final String input) {
112         Matcher matcher = QNAME_PATTERN_FULL.matcher(input);
113         if (matcher.matches()) {
114             final String namespace = matcher.group(1);
115             final String revision = matcher.group(2);
116             final String localName = matcher.group(3);
117             return create(namespace, revision, localName);
118         }
119         matcher = QNAME_PATTERN_NO_REVISION.matcher(input);
120         if (matcher.matches()) {
121             final URI namespace = URI.create(matcher.group(1));
122             final String localName = matcher.group(2);
123             return new QName(namespace, localName);
124         }
125         throw new IllegalArgumentException("Invalid input: " + input);
126     }
127
128     public static @NonNull QName create(final QName base, final String localName) {
129         return create(base.getModule(), localName);
130     }
131
132     /**
133      * Creates new QName.
134      *
135      * @param qnameModule Namespace and revision enclosed as a QNameModule
136      * @param localName Local name part of QName. MUST NOT BE null.
137      * @return Instance of QName
138      * @throws NullPointerException if any argument is null
139      * @throws IllegalArgumentException if localName is not a valid YANG identifier
140      */
141     public static @NonNull QName create(final QNameModule qnameModule, final String localName) {
142         return new QName(requireNonNull(qnameModule, "module may not be null"), checkLocalName(localName));
143     }
144
145     /**
146      * Creates new QName.
147      *
148      * @param namespace Namespace of QName or null if namespace is undefined.
149      * @param revision Revision of namespace or null if revision is unspecified.
150      * @param localName Local name part of QName. MUST NOT BE null.
151      * @return Instance of QName
152      */
153     public static @NonNull QName create(final URI namespace, final @Nullable Revision revision,
154             final String localName) {
155         return create(QNameModule.create(namespace, revision), localName);
156     }
157
158     /**
159      * Creates new QName.
160      *
161      * @param namespace Namespace of QName or null if namespace is undefined.
162      * @param revision Revision of namespace.
163      * @param localName Local name part of QName. MUST NOT BE null.
164      * @return Instance of QName
165      */
166     public static @NonNull QName create(final URI namespace, final Optional<Revision> revision,
167             final String localName) {
168         return create(QNameModule.create(namespace, revision), localName);
169     }
170
171     /**
172      * Creates new QName.
173      *
174      * @param namespace Namespace of QName or null if namespace is undefined.
175      * @param revision Revision of namespace or null if revision is unspecified.
176      * @param localName Local name part of QName. MUST NOT BE null.
177      * @return Instance of QName
178      */
179     public static @NonNull QName create(final String namespace, final String localName, final Revision revision) {
180         return create(QNameModule.create(parseNamespace(namespace), revision), localName);
181     }
182
183     /**
184      * Creates new QName.
185      *
186      * @param namespace Namespace of QName, MUST NOT BE Null.
187      * @param revision Revision of namespace / YANG module. MUST NOT BE null, MUST BE in format {@code YYYY-mm-dd}.
188      * @param localName Local name part of QName. MUST NOT BE null.
189      * @return A new QName
190      * @throws NullPointerException If any of parameters is null.
191      * @throws IllegalArgumentException If {@code namespace} is not valid URI or {@code revision} does not conform
192      *         to {@code YYYY-mm-dd}.
193      */
194     public static @NonNull QName create(final String namespace, final String revision, final String localName) {
195         return create(parseNamespace(namespace), Revision.of(revision), localName);
196     }
197
198     /**
199      * Creates new QName.
200      *
201      * @param namespace Namespace of QName, MUST NOT BE Null.
202      * @param localName Local name part of QName. MUST NOT BE null.
203      * @return A new QName
204      * @throws NullPointerException If any of parameters is null.
205      * @throws IllegalArgumentException If {@code namespace} is not valid URI.
206      */
207     public static @NonNull QName create(final String namespace, final String localName) {
208         return create(parseNamespace(namespace), localName);
209     }
210
211     /**
212      * Creates new QName.
213      *
214      * @param namespace Namespace of QName, MUST NOT BE null.
215      * @param localName Local name part of QName. MUST NOT BE null.
216      * @return A new QName
217      * @throws NullPointerException If any of parameters is null.
218      * @throws IllegalArgumentException If <code>namespace</code> is not valid URI.
219      */
220     public static @NonNull QName create(final URI namespace, final String localName) {
221         return new QName(namespace, localName);
222     }
223
224     /**
225      * Read a QName from a DataInput. The format is expected to match the output format of {@link #writeTo(DataOutput)}.
226      *
227      * @param in DataInput to read
228      * @return A QName instance
229      * @throws IOException if I/O error occurs
230      */
231     public static @NonNull QName readFrom(final DataInput in) throws IOException {
232         if (in instanceof QNameAwareDataInput) {
233             return ((QNameAwareDataInput) in).readQName();
234         }
235
236         final QNameModule module = QNameModule.readFrom(in);
237         return new QName(module, checkLocalName(in.readUTF()));
238     }
239
240     /**
241      * Creates new QName composed of specified module and local name. This method does not perform lexical checking of
242      * localName, and it is the caller's responsibility to performs these checks.
243      *
244      * <p>
245      * When in doubt, use {@link #create(QNameModule, String)} instead.
246      *
247      * @param qnameModule Namespace and revision enclosed as a QNameModule
248      * @param localName Local name part of QName, required to have been validated
249      * @return Instance of QName
250      * @throws NullPointerException if any of the arguments is null
251      */
252     @Beta
253     public static @NonNull QName unsafeOf(final @NonNull QNameModule qnameModule, final @NonNull String localName) {
254         return new QName(qnameModule, localName);
255     }
256
257     /**
258      * Get the module component of the QName.
259      *
260      * @return Module component
261      */
262     public @NonNull QNameModule getModule() {
263         return module;
264     }
265
266     /**
267      * Returns XMLNamespace assigned to the YANG module.
268      *
269      * @return XMLNamespace assigned to the YANG module.
270      */
271     public @NonNull URI getNamespace() {
272         return module.getNamespace();
273     }
274
275     /**
276      * Returns revision of the YANG module if the module has defined revision.
277      *
278      * @return revision of the YANG module if the module has defined revision.
279      */
280     public @NonNull Optional<Revision> getRevision() {
281         return module.getRevision();
282     }
283
284     @Override
285     public @NonNull QName intern() {
286         // We also want to make sure we keep the QNameModule cached
287         final QNameModule cacheMod = module.intern();
288
289         // Identity comparison is here on purpose, as we are deciding whether to potentially store 'qname' into the
290         // interner. It is important that it does not hold user-supplied reference (such a String instance from
291         // parsing of an XML document).
292         final QName template = cacheMod == module ? this : new QName(cacheMod, getLocalName().intern());
293
294         return INTERNER.intern(template);
295     }
296
297     @Override
298     public int hashCode() {
299         if (hash == 0) {
300             hash = Objects.hash(module, getLocalName());
301         }
302         return hash;
303     }
304
305     /**
306      * Compares the specified object with this list for equality.  Returns {@code true} if and only if the specified
307      * object is also instance of {@link QName} and its {@link #getLocalName()}, {@link #getNamespace()} and
308      * {@link #getRevision()} are equals to same properties of this instance.
309      *
310      * @param obj the object to be compared for equality with this QName
311      * @return {@code true} if the specified object is equal to this QName
312      */
313     @Override
314     public boolean equals(final Object obj) {
315         if (this == obj) {
316             return true;
317         }
318         if (!(obj instanceof QName)) {
319             return false;
320         }
321         final QName other = (QName) obj;
322         return Objects.equals(getLocalName(), other.getLocalName()) && module.equals(other.module);
323     }
324
325     private static @NonNull URI parseNamespace(final String namespace) {
326         try {
327             return new URI(namespace);
328         } catch (final URISyntaxException ue) {
329             throw new IllegalArgumentException("Namespace '" + namespace + "' is not a valid URI", ue);
330         }
331     }
332
333     @Override
334     public @NonNull String toString() {
335         final StringBuilder sb = new StringBuilder().append(QNAME_LEFT_PARENTHESIS).append(getNamespace());
336         final Optional<Revision> rev = getRevision();
337         if (rev.isPresent()) {
338             sb.append(QNAME_REVISION_DELIMITER).append(rev.get());
339         }
340         return sb.append(QNAME_RIGHT_PARENTHESIS).append(getLocalName()).toString();
341     }
342
343     @Override
344     public @NonNull QName bindTo(final QNameModule namespace) {
345         return module.equals(namespace) ? this : super.bindTo(namespace);
346     }
347
348     /**
349      * Returns a QName with the same namespace and local name, but with no revision. If this QName does not have
350      * a Revision, this object is returned.
351      *
352      * @return a QName with the same namespace and local name, but with no revision.
353      */
354     public @NonNull QName withoutRevision() {
355         final QNameModule newModule;
356         return (newModule = module.withoutRevision()) == module ? this : new QName(newModule, getLocalName());
357     }
358
359     /**
360      * Formats {@link Revision} representing revision to format {@code YYYY-mm-dd}
361      *
362      * <p>
363      * YANG Specification defines format for {@code revision<} as YYYY-mm-dd. This format for revision is reused across
364      * multiple places such as capabilities URI, YANG modules, etc.
365      *
366      * @param revision Date object to format
367      * @return String representation or null if the input was null.
368      */
369     public static @Nullable String formattedRevision(final Optional<Revision> revision) {
370         return revision.map(Revision::toString).orElse(null);
371     }
372
373     /**
374      * Compares this QName to other, without comparing revision.
375      *
376      * <p>
377      * Compares instance of this to other instance of QName and returns true if both instances have equal
378      * {@code localName} ({@link #getLocalName()}) and @{code namespace} ({@link #getNamespace()}).
379      *
380      * @param other Other QName. Must not be null.
381      * @return true if this instance and other have equals localName and namespace.
382      * @throws NullPointerException if {@code other} is null.
383      */
384     public boolean isEqualWithoutRevision(final QName other) {
385         return getLocalName().equals(other.getLocalName()) && Objects.equals(getNamespace(), other.getNamespace());
386     }
387
388     // FIXME: this comparison function looks odd. We are sorting first by local name and then by module? What is
389     //        the impact on iteration order of SortedMap<QName, ?>?
390     @Override
391     @SuppressWarnings("checkstyle:parameterName")
392     public int compareTo(final QName o) {
393         // compare mandatory localName parameter
394         int result = getLocalName().compareTo(o.getLocalName());
395         if (result != 0) {
396             return result;
397         }
398         return module.compareTo(o.module);
399     }
400
401     @Override
402     public void writeTo(final DataOutput out) throws IOException {
403         if (out instanceof QNameAwareDataOutput) {
404             ((QNameAwareDataOutput) out).writeQName(this);
405         } else {
406             module.writeTo(out);
407             out.writeUTF(getLocalName());
408         }
409     }
410
411     @Override
412     Object writeReplace() {
413         return new QNv1(this);
414     }
415 }