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