Define a feature-parent
[yangtools.git] / common / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / Revision.java
1 /*
2  * Copyright (c) 2016 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 java.io.DataInput;
13 import java.io.Externalizable;
14 import java.io.IOException;
15 import java.io.NotSerializableException;
16 import java.io.ObjectInput;
17 import java.io.ObjectInputStream;
18 import java.io.ObjectOutput;
19 import java.io.ObjectOutputStream;
20 import java.io.ObjectStreamException;
21 import java.time.format.DateTimeFormatter;
22 import java.time.format.DateTimeParseException;
23 import java.util.Optional;
24 import java.util.regex.Pattern;
25 import org.checkerframework.checker.regex.qual.Regex;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.Nullable;
28
29 /**
30  * Dedicated object identifying a YANG module revision.
31  *
32  * <h2>API design note</h2>
33  * This class defines the contents of a revision statement, but modules do not require to have a revision (e.g. they
34  * have not started to keep track of revisions).
35  *
36  * <p>
37  * APIs which involve this class should always transfer instances via {@code Optional<Revision>}, which is
38  * the primary bridge data type. Implementations can use nullable fields with explicit conversions to/from
39  * {@link Optional}. Both patterns can take advantage of {@link #compare(Optional, Optional)} and
40  * {@link #compare(Revision, Revision)} respectively.
41  */
42 public final class Revision implements RevisionUnion {
43     // Note: since we are using writeReplace() this version is not significant.
44     @java.io.Serial
45     private static final long serialVersionUID = 1L;
46
47     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
48
49     @Regex
50     // FIXME: we should improve this to filter incorrect dates -- see constructor.
51     private static final String STRING_FORMAT_PATTERN_STR = "\\d\\d\\d\\d\\-\\d\\d-\\d\\d";
52
53     /**
54      * String format pattern, which can be used to match parts of a string into components.
55      */
56     public static final Pattern STRING_FORMAT_PATTERN = Pattern.compile(STRING_FORMAT_PATTERN_STR);
57
58     /**
59      * Revision which compares as greater than any other valid revision.
60      */
61     public static final Revision MAX_VALUE = Revision.of("9999-12-31");
62
63     private final @NonNull String str;
64
65     private Revision(final @NonNull String str) {
66         /*
67          * According to RFC7950 (https://www.rfc-editor.org/rfc/rfc7950#section-7.1.9):
68          *
69          *   The "revision" statement specifies the editorial revision history of
70          *   the module, including the initial revision.  A series of "revision"
71          *   statements detail the changes in the module's definition.  The
72          *   argument is a date string in the format "YYYY-MM-DD", [...]
73          *
74          * Hence we use JDK-provided parsing faculties to parse the date.
75          */
76         FORMATTER.parse(str);
77         this.str = str;
78     }
79
80     /**
81      * Parse a revision string.
82      *
83      * @param str String to be parsed
84      * @return A Revision instance.
85      * @throws DateTimeParseException if the string format does not conform specification.
86      * @throws NullPointerException if the string is null
87      */
88     public static @NonNull Revision of(final @NonNull String str) {
89         return new Revision(str);
90     }
91
92     /**
93      * Parse a (potentially null) revision string. Null strings result result in {@link Optional#empty()}.
94      *
95      * @param str String to be parsed
96      * @return An optional Revision instance.
97      * @throws DateTimeParseException if the string format does not conform specification.
98      */
99     public static @NonNull Optional<Revision> ofNullable(final @Nullable String str) {
100         return str == null ? Optional.empty() : Optional.of(new Revision(str));
101     }
102
103     @Override
104     public Revision revision() {
105         return this;
106     }
107
108     @Override
109     public String unionString() {
110         return str;
111     }
112
113     public static @NonNull Revision readFrom(final DataInput in) throws IOException {
114         return ofRead(in.readUTF());
115     }
116
117     static @NonNull Revision ofRead(final @NonNull String str) throws IOException {
118         try {
119             return of(str);
120         } catch (DateTimeParseException e) {
121             throw new IOException("Invalid revision-date string", e);
122         }
123     }
124
125     /**
126      * Compare two {@link Optional}s wrapping Revisions. Arguments and return value are consistent with
127      * {@link java.util.Comparator#compare(Object, Object)} interface contract. Missing revisions compare as lower
128      * than any other revision.
129      *
130      * @param first First optional revision
131      * @param second Second optional revision
132      * @return Positive, zero, or negative integer.
133      */
134     public static int compare(final @NonNull Optional<Revision> first, final @NonNull Optional<Revision> second) {
135         if (first.isPresent()) {
136             return second.isPresent() ? first.orElseThrow().compareTo(second.orElseThrow()) : 1;
137         }
138         return second.isPresent() ? -1 : 0;
139     }
140
141     /**
142      * Compare two explicitly nullable Revisions. Unlike {@link #compareTo(Revision)}, this handles both arguments
143      * being null such that total ordering is defined.
144      *
145      * @param first First revision
146      * @param second Second revision
147      * @return Positive, zero, or negative integer.
148      */
149     public static int compare(final @Nullable Revision first, final @Nullable Revision second) {
150         if (first != null) {
151             return second != null ? first.compareTo(second) : 1;
152         }
153         return second != null ? -1 : 0;
154     }
155
156     @Override
157     public int hashCode() {
158         return str.hashCode();
159     }
160
161     @Override
162     public boolean equals(final Object obj) {
163         return this == obj || obj instanceof Revision other && str.equals(other.str);
164     }
165
166     /**
167      * Return the revision string according to
168      * <a href="https://www.rfc-editor.org/rfc/rfc7950.html#section-7.1.9">RFC7950, section 7.1.9</a>. The string is
169      * guaranteed to be composed on 10 ASCII characters in {@code YYYY-MM-DD} format. The items are guaranteed to be
170      * decimal numbers. There are no further guarantees as to convertibility to an actual calendar date, as this method
171      * can validly return {@code "2019-02-29"} (even if 2019 is not a leap year) or {@code "2022-12-32"} (even if no
172      * month has 32 days).
173      *
174      * @return A string in {@code YYYY-MM-DD} format
175      */
176     @Override
177     public String toString() {
178         return str;
179     }
180
181     @java.io.Serial
182     Object writeReplace() {
183         return new RUv1(str);
184     }
185
186     @java.io.Serial
187     private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
188         throwNSE();
189     }
190
191     @java.io.Serial
192     private void readObjectNoData() throws ObjectStreamException {
193         throwNSE();
194     }
195
196     @java.io.Serial
197     private void writeObject(final ObjectOutputStream stream) throws IOException {
198         throwNSE();
199     }
200
201     static void throwNSE() throws NotSerializableException {
202         throw new NotSerializableException(Revision.class.getName());
203     }
204
205     @Deprecated(since = "12.0.0", forRemoval = true)
206     private static final class Proxy implements Externalizable {
207         @java.io.Serial
208         private static final long serialVersionUID = 1L;
209
210         private Revision revision;
211
212         @SuppressWarnings("checkstyle:redundantModifier")
213         public Proxy() {
214             // For Externalizable
215         }
216
217         @Override
218         public void writeExternal(final ObjectOutput out) throws IOException {
219             throw new NotSerializableException(Proxy.class.getName());
220         }
221
222         @Override
223         public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
224             revision = Revision.of((String) in.readObject());
225         }
226
227         @java.io.Serial
228         private Object readResolve() {
229             return requireNonNull(revision);
230         }
231     }
232 }