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