BUG-4688: update Revision class design 81/64681/4
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 24 Oct 2017 22:45:20 +0000 (00:45 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 25 Oct 2017 16:53:21 +0000 (18:53 +0200)
Since Revision is typically captured property, with complete semantics
when it is missing, it is useful to define operations on nullable and
optional Revisions.

Furthermore we know what the proper format is, so rather than relying
on a date object, use the underlying string. This saves us some work
when communicating with others.

Change-Id: I5716f416cf87697f832c05d6223dbc9dd87dfd15
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/Revision.java

index 54279d7a497b6aeea4bd8245be9f281478343699..7bd1fe425fadfff59b8c1b1b2dd7e348766decbb 100644 (file)
@@ -9,63 +9,116 @@ package org.opendaylight.yangtools.yang.common;
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.base.Preconditions;
 import java.io.Externalizable;
 import java.io.IOException;
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
 import java.io.Serializable;
-import java.text.ParseException;
-import java.util.Date;
+import java.util.Optional;
+import java.util.regex.Pattern;
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.RegEx;
 
 /**
  * Dedicated object identifying a YANG module revision.
  *
+ * <p>
+ * <h3>API design note</h3>
+ * This class defines the contents of a revision statement, but modules do not require to have a revision (e.g. they
+ * have not started to keep track of revisions).
+ *
+ * <p>
+ * APIs which involve this class should always transfer instances via {@code Optional<Revision>}, which is
+ * the primary bridge data type. Implementations can use nullable fields with explicit conversions to/from
+ * {@link Optional}. Both patterns can take advantage of {@link #compare(Optional, Optional)} and
+ * {@link #compare(Revision, Revision)} respectively.
+ *
  * @author Robert Varga
  */
-public abstract class Revision implements Comparable<Revision>, Serializable {
+public final class Revision implements Comparable<Revision>, Serializable {
     private static final long serialVersionUID = 1L;
 
+    @RegEx
+    private static final String STRING_FORMAT_STR = "\\d\\d\\d\\d\\-\\d\\d-\\d\\d";
+    private static final Pattern STRING_FORMAT = Pattern.compile(STRING_FORMAT_STR);
+
+    private final String str;
+
+    private Revision(final String str) {
+        // Since all strings conform to this format, compareTo() can be delegated to String.compareTo()
+        Preconditions.checkArgument(STRING_FORMAT.matcher(str).matches(), "String '%s' does match revision format",
+            str);
+        this.str = str;
+    }
+
     /**
-     * Legacy implementation.
+     * Parse a revision string.
      *
-     * @author Robert Varga
+     * @param str String to be parsed
+     * @return A Revision instance.
+     * @throws IllegalArgumentException if the string format does not conform specification.
+     * @throws NullPointerException if the string is null
      */
-    private static final class ForDate extends Revision {
-        private static final long serialVersionUID = 1L;
-
-        private final Date date;
-        private String str;
+    public static Revision valueOf(@Nonnull final String str) {
+        return new Revision(str);
+    }
 
-        ForDate(final Date date) {
-            this.date = requireNonNull(date);
+    /**
+     * Compare two {@link Optional}s wrapping Revisions. Arguments and return value are consistent with
+     * {@link java.util.Comparator#compare(Object, Object)} interface contract. Missing revisions compare as lower
+     * than any other revision.
+     *
+     * @param first First optional revision
+     * @param second Second optional revision
+     * @return Positive, zero, or negative integer.
+     */
+    public static int compare(final Optional<Revision> first, final Optional<Revision> second) {
+        if (first.isPresent()) {
+            return second.isPresent() ? first.get().compareTo(second.get()) : 1;
         }
+        return second.isPresent() ? -1 : 0;
+    }
 
-        ForDate(final Date date, final String str) {
-            this.date = requireNonNull(date);
-            this.str = requireNonNull(str);
+    /**
+     * Compare two explicitly nullable Revisions. Unlike {@link #compareTo(Revision)}, this handles both arguments
+     * being null such that total ordering is defined.
+     *
+     * @param first First revision
+     * @param second Second revision
+     * @return Positive, zero, or negative integer.
+     */
+    public static int compare(@Nullable final Revision first, @Nullable final Revision second) {
+        if (first != null) {
+            return second != null ? first.compareTo(second) : 1;
         }
+        return second != null ? -1 : 0;
+    }
 
-        @Override
-        public Date toDate() {
-            return date;
-        }
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public int compareTo(final Revision o) {
+        return str.compareTo(o.str);
+    }
 
-        @Override
-        public String toString() {
-            String ret = str;
-            if (ret == null) {
-                synchronized (this) {
-                    ret = str;
-                    if (ret == null) {
-                        ret = SimpleDateFormatUtil.getRevisionFormat().format(date);
-                        str = ret;
-                    }
-                }
-            }
-
-            return ret;
-        }
+    @Override
+    public int hashCode() {
+        return str.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this == obj || obj instanceof Revision && str.equals(((Revision)obj).str);
+    }
+
+    @Override
+    public String toString() {
+        return str;
+    }
+
+    Object writeReplace() {
+        return new Proxy(str);
     }
 
     private static final class Proxy implements Externalizable {
@@ -93,63 +146,7 @@ public abstract class Revision implements Comparable<Revision>, Serializable {
         }
 
         private Object readResolve() {
-            try {
-                return Revision.forString(str);
-            } catch (ParseException e) {
-                throw new RuntimeException(e);
-            }
+            return Revision.valueOf(str);
         }
     }
-
-    Revision() {
-        // Hidden from the world
-    }
-
-    /**
-     * Convert a Date into a Revision.
-     *
-     * @param date Input date
-     * @return A Revision instance.
-     *
-     * @deprecated Transition bridge method to ease transition from Date.
-     */
-    @Deprecated
-    public static Revision forDate(@Nonnull final Date date) {
-        return new ForDate(date);
-    }
-
-    /**
-     * Parse a revision string.
-     *
-     * @param str String to be parsed
-     * @return A Revision instance.
-     * @throws ParseException if the string format does not conform specification.
-     */
-    public static Revision forString(@Nonnull final String str) throws ParseException {
-        final Date date = SimpleDateFormatUtil.getRevisionFormat().parse(str);
-        return new ForDate(date, str);
-    }
-
-    @Override
-    @SuppressWarnings("checkstyle:parameterName")
-    public final int compareTo(final Revision o) {
-        return toDate().compareTo(o.toDate());
-    }
-
-    /**
-     * Convert this Revision to a Date object. The returned Date will be in UTC.
-     *
-     * @return Data representation of this Revision
-     *
-     * @deprecated Transition bridge method to ease transition from Date.
-     */
-    @Deprecated
-    @Nonnull public abstract Date toDate();
-
-    @Override
-    public abstract String toString();
-
-    final Object writeReplace() {
-        return new Proxy(toString());
-    }
 }