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