Add yang.common.DerivedString class 29/70229/18
authorRobert Varga <robert.varga@pantheon.tech>
Sat, 31 Mar 2018 23:04:47 +0000 (01:04 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 9 Apr 2018 11:42:29 +0000 (13:42 +0200)
DerivedString provides the baseline alternative for storing
String-equivalent information. The base class provides to prototype
using which interactions with Strings can be implemented.

Each such type is supported by a DerivedStringSupport, potentially
further validated via a DerivedStringValidator.

JIRA: YANGTOOLS-418
Change-Id: Ifa8c01723fdc43e71cc6ab48fca50963a131ccaa
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringSupport.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringValidator.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/CachingDerivedString.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedString.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringSupport.java [new file with mode: 0644]
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringValidator.java [new file with mode: 0644]
yang/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/DerivedStringTest.java [new file with mode: 0644]

diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringSupport.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringSupport.java
new file mode 100644 (file)
index 0000000..56e4bac
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import java.lang.reflect.Modifier;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Base implementation of {@link DerivedStringSupport}. This class should be used as superclass to all implementations
+ * of {@link DerivedStringSupport}, as doing so provides a simpler base and enforces some aspects of the subclass.
+ *
+ * @param <T> derived string type
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public abstract class AbstractDerivedStringSupport<T extends DerivedString<T>> implements DerivedStringSupport<T> {
+    private static final ClassValue<Boolean> VALIDATED_INSTANCES = new ClassValue<Boolean>() {
+        @Override
+        protected Boolean computeValue(final @Nullable Class<?> type) {
+            // Every DerivedStringSupport representation class must:
+            checkArgument(DerivedStringSupport.class.isAssignableFrom(type), "%s is not a DerivedStringSupport", type);
+
+            // be final
+            final int modifiers = type.getModifiers();
+            checkArgument(Modifier.isFinal(modifiers), "%s must be final", type);
+
+            return Boolean.TRUE;
+        }
+    };
+
+    private final Class<T> representationClass;
+
+    protected AbstractDerivedStringSupport(final Class<T> representationClass) {
+        this.representationClass = DerivedString.validateRepresentationClass(representationClass);
+        VALIDATED_INSTANCES.get(getClass());
+    }
+
+    @Override
+    public final Class<T> getRepresentationClass() {
+        return representationClass;
+    }
+
+    @Override
+    public final Class<T> getValidatedRepresentationClass() {
+        return representationClass;
+    }
+
+    @Override
+    public final T validateRepresentation(final T value) {
+        return requireNonNull(value);
+    }
+
+    @Override
+    public final T validateRepresentation(final T value, final String canonicalString) {
+        return requireNonNull(value);
+    }
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringValidator.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringValidator.java
new file mode 100644 (file)
index 0000000..40c98f9
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Abstract base class for implementing validators.
+ *
+ * @param <R> string representation class
+ * @param <T> validated string representation class
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public abstract class AbstractDerivedStringValidator<R extends DerivedString<R>, T extends R>
+        implements DerivedStringValidator<R, T> {
+    private final DerivedStringSupport<R> representationSupport;
+    private final Class<T> validatedClass;
+
+    protected AbstractDerivedStringValidator(final DerivedStringSupport<R> representationSupport,
+            final Class<T> validatedClass) {
+        this.representationSupport = requireNonNull(representationSupport);
+        this.validatedClass = DerivedString.validateRepresentationClass(validatedClass);
+    }
+
+    @Override
+    public final Class<R> getRepresentationClass() {
+        return representationSupport.getRepresentationClass();
+    }
+
+    @Override
+    public final Class<T> getValidatedRepresentationClass() {
+        return validatedClass;
+    }
+
+    @Override
+    public final T validateRepresentation(final R value) {
+        @Nullable T valid;
+        return (valid = castIfValid(value)) != null ? valid : validate(value);
+    }
+
+    @Override
+    public final T validateRepresentation(final R value, final String canonicalString) {
+        @Nullable T valid;
+        return (valid = castIfValid(value)) != null ? valid : validate(value, requireNonNull(canonicalString));
+    }
+
+    /**
+     * Validate a {@link DerivedString} representation. Subclasses should override this method if they can
+     * provide a validation algorithm which does not rely on canonical strings but works on representation state only.
+     *
+     * @param value Representation value
+     * @return Validated representation
+     * @throws NullPointerException if {@code value} is null
+     * @throws IllegalArgumentException if the value does not meet validation criteria.
+     */
+    protected T validate(final R value) {
+        return validate(value, value.toCanonicalString());
+    }
+
+    /**
+     * Validate a {@link DerivedString} representation. Subclasses can chose whether they operate on representation
+     * state or canonical string -- both are considered equivalent.
+     *
+     * @param value Representation value
+     * @param canonicalString Canonical string matching the representation value
+     * @return Validated representation
+     * @throws NullPointerException if {@code value} or {@code canonicalString} is null.
+     * @throws IllegalArgumentException if the value does not meet validation criteria.
+     */
+    protected abstract T validate(R value, String canonicalString);
+
+    private @Nullable T castIfValid(final R value) {
+        return validatedClass.isAssignableFrom(value.validator().getValidatedRepresentationClass())
+                ? validatedClass.cast(value) : null;
+    }
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/CachingDerivedString.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/CachingDerivedString.java
new file mode 100644 (file)
index 0000000..3a15c2a
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * A opportunistically-caching {@link DerivedString}. Canonical name is cached at first encounter.
+ *
+ * @param <T> derived string type
+ * @author Robert Varga
+ */
+@NonNullByDefault
+public abstract class CachingDerivedString<T extends CachingDerivedString<T>> extends DerivedString<T> {
+    private static final long serialVersionUID = 1L;
+
+    private transient volatile @Nullable String str;
+
+    protected CachingDerivedString() {
+
+    }
+
+    protected CachingDerivedString(final String str) {
+        this.str = requireNonNull(str);
+    }
+
+    @Override
+    public final String toCanonicalString() {
+        String local;
+        return (local = this.str) != null ? local : (str = computeCanonicalString());
+    }
+
+    /**
+     * Return the canonical string representation of this object's value.
+     *
+     * @return Canonical string
+     */
+    protected abstract String computeCanonicalString();
+
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedString.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedString.java
new file mode 100644 (file)
index 0000000..3dc1522
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.Beta;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for objects which are string-equivalent to canonical string representation specified
+ * in a YANG model. Note that each subclass of {@link DerivedString} defines its own {@link #hashCode()} and
+ * {@link #equals(Object)} contracts based on implementation particulars.
+ *
+ * <p>
+ * Since YANG validation works on top of strings, which in itself is expensive and this class provides storage which
+ * is potentially not based on strings, its design combines 'representation' and 'validated to match constraints'
+ * aspects of a YANG type derived from string. To achieve that it cooperates with {@link DerivedStringValidator} and
+ * {@link DerivedStringSupport}.
+ *
+ * <p>
+ * Given the following YANG snippet:
+ * <pre>
+ *     typedef foo {
+ *         type string;
+ *         pattern "[1-9]?[0-9]";
+ *     }
+ *
+ *     typedef bar {
+ *         type foo;
+ *         patter "[1-9][0-9]";
+ *     }
+ *
+ *     typedef baz {
+ *         type foo;
+ *     }
+ * </pre>
+ * it is obvious we could use a storage class with 'int' as the internal representation of all three types and define
+ * operations on top of it. In this case we would define:
+ * <ul>
+ *     <li>{@code public class FooDerivedString extends DerivedString<FooDerivedString>}, which implements all abstract
+ *         methods of {@link DerivedString} as final methods. It will notably not override {@link #validator()} and
+ *         must not be final.</li>
+ *     <li>{@code public final class FooDerivedStringSupport extends DerivedStringSupport<FooDerivedString>}, which
+ *         forms the baseline validator and instantiation for {@code FooDerivedString}. It should be a singleton class
+ *         with a getInstance() method.</li>
+ *     <li>{@code public class BarDerivedString extends FooDerivedString}, which overrides {@link #validator()} to
+ *         indicate its contents have been validated to conform to bar -- it does that by returning the singleton
+ *         instance of {@code BarDerivedStringValidator}.
+ *     <li>{@code public final class BarDerivedStringValidator extends DerivedStringValidator<FooDerivedString,
+ *         BarDerivedString}. This method needs to notably implement
+ *         {@link DerivedStringValidator#validateRepresentation(DerivedString)} to hand out BarDerivedString instances.
+ *         This class needs to be a singleton with a getInstance() method, too.</li>
+ * </ul>
+ * Since {@code baz} is not defining any new restrictions, all instances of FooDerivedString are valid for it and we
+ * do not have to define any additional support.
+ *
+ * <p>
+ * It is important for {@link DerivedString} subclasses not to be final because any YANG type can be further extended
+ * and adding a final class in that hierarchy would prevent a proper class from being defined.
+ *
+ * @param <R> derived string representation
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public abstract class DerivedString<R extends DerivedString<R>> implements Comparable<R>, Immutable, Serializable {
+    private static final class Validator extends ClassValue<Boolean> {
+        private static final Logger LOG = LoggerFactory.getLogger(Validator.class);
+
+        @Override
+        protected Boolean computeValue(final @Nullable Class<?> type) {
+            // Every DerivedString representation class must:
+            checkArgument(DerivedString.class.isAssignableFrom(type), "%s is not a DerivedString", type);
+
+            // be non-final and public
+            final int modifiers = type.getModifiers();
+            checkArgument(Modifier.isPublic(modifiers), "%s must be public", type);
+            checkArgument(!Modifier.isFinal(modifiers), "%s must not be final", type);
+
+            // have at least one public or protected constructor (for subclasses)
+            checkArgument(Arrays.stream(type.getDeclaredConstructors()).mapToInt(Constructor::getModifiers)
+                .anyMatch(mod -> Modifier.isProtected(mod) || Modifier.isPublic(mod)),
+                "%s must declare at least one protected or public constructor", type);
+
+            try {
+                // have a non-final non-abstract validator() method
+                final int validator;
+                try {
+                    validator = type.getMethod("validator").getModifiers();
+                } catch (NoSuchMethodException e) {
+                    throw new IllegalArgumentException(type + " must have a non-abstract non-final validator() method",
+                        e);
+                }
+                checkArgument(!Modifier.isFinal(validator), "%s must not have final validator()", type);
+
+                // have final toCanonicalString(), support(), hashCode() and equals(Object), compare(T) methods
+                checkFinalMethod(type, "toCanonicalString");
+                checkFinalMethod(type, "support");
+                checkFinalMethod(type, "hashCode");
+                checkFinalMethod(type, "equals", Object.class);
+                checkFinalMethod(type, "compareTo", type);
+            } catch (SecurityException e) {
+                LOG.warn("Cannot completely validate {}", type, e);
+                return Boolean.FALSE;
+            }
+
+            return Boolean.TRUE;
+        }
+
+        private static void checkFinalMethod(final Class<?> type, final String name) {
+            try {
+                checkFinalMethod(type.getMethod(name).getModifiers(), type, name, "");
+            } catch (NoSuchMethodException e) {
+                throw new IllegalArgumentException(type + " must have a final " + name + "() method", e);
+            }
+        }
+
+        private static void checkFinalMethod(final Class<?> type, final String name, final Class<?> arg) {
+            final String argName = arg.getSimpleName();
+            try {
+                checkFinalMethod(type.getMethod(name, arg).getModifiers(), type, name, argName);
+            } catch (NoSuchMethodException e) {
+                throw new IllegalArgumentException(type + " must have a final " + name + "(" + argName + ") method", e);
+            }
+        }
+
+        private static void checkFinalMethod(final int modifiers, final Class<?> type, final String name,
+                final String args) {
+            checkArgument(Modifier.isFinal(modifiers), "%s must have a final %s(%s) method", type, name, args);
+        }
+    }
+
+    private static final ClassValue<Boolean> VALIDATED_REPRESENTATIONS = new Validator();
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Return the canonical string representation of this object's value.
+     *
+     * @return Canonical string
+     */
+    public abstract String toCanonicalString();
+
+    /**
+     * Return the {@link DerivedStringSupport} associated with this type. It can be used to create new instances of this
+     * representation.
+     *
+     * @return A {@link DerivedStringSupport} instance.
+     */
+    public abstract DerivedStringSupport<R> support();
+
+    /**
+     * Return a {@link DerivedStringValidator} associated with this value's validated type.
+     *
+     * @return A {@link DerivedStringValidator} instance.
+     */
+    public DerivedStringValidator<R, ? extends R> validator() {
+        return support();
+    }
+
+    @Override
+    public abstract int hashCode();
+
+    @Override
+    public abstract boolean equals(@Nullable Object obj);
+
+    @Override
+    public final String toString() {
+        return toCanonicalString();
+    }
+
+    static <T extends DerivedString<?>> Class<T> validateRepresentationClass(final Class<T> representationClass) {
+        // Validation is reflective, cache its result
+        VALIDATED_REPRESENTATIONS.get(representationClass);
+        return representationClass;
+    }
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringSupport.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringSupport.java
new file mode 100644 (file)
index 0000000..56afaf6
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Support for a {@link DerivedString} subclasses. An implementation of this interface must be registered
+ * in the system and be available from each DerivedString object.
+ *
+ * <p>
+ * Note: never implement this interface directly, subclass {@link AbstractDerivedStringSupport} instead.
+ *
+ * <p>
+ * This interface allows a {@link DerivedString} to be instantiated from a String. The implementation is expected
+ * to perform all checks implied by the corresponding YANG data model.
+ *
+ * @param <R> derived string representation
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+@ThreadSafe
+public interface DerivedStringSupport<R extends DerivedString<R>> extends DerivedStringValidator<R, R> {
+    /**
+     * Create a instance for a string representation. Implementations of this method are required to perform checks
+     * equivalent to the YANG data model restrictions attached to the corresponding YANG type. Non-canonical format
+     * strings must be accepted and result in objects equal to objects obtained from the corresponding canonical format.
+     *
+     * @param str String representation
+     * @return A {@link DerivedString} instance.
+     * @throws NullPointerException if str is null
+     * @throws IllegalArgumentException if str does not contain a valid representation
+     */
+    R fromString(String str);
+
+    /**
+     * Create a instance for the canonical string representation. Implementations of this method may perform
+     * optimizations based on the assumption the string is canonical, but should still report errors when a mismatch
+     * is detected.
+     *
+     * @param str String representation
+     * @return A {@link DerivedString} instance.
+     * @throws NullPointerException if str is null
+     * @throws IllegalArgumentException if str does not contain canonical representation
+     */
+    default R fromCanonicalString(final String str) {
+        return fromString(requireNonNull(str));
+    }
+
+    /**
+     * Unsafe cast to a factory type.
+     *
+     * @return This instance cast to specified type
+     */
+    @SuppressWarnings("unchecked")
+    default <X extends DerivedString<X>> DerivedStringSupport<X> unsafe() {
+        return (DerivedStringSupport<X>) this;
+    }
+}
diff --git a/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringValidator.java b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringValidator.java
new file mode 100644 (file)
index 0000000..e585328
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import com.google.common.annotations.Beta;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * YANG string representation validator. Implementations of this interface can perform further validation of
+ * representation state such that it conforms to a YANG string type derived from a type with a {@link DerivedString}
+ * representation class.
+ *
+ * <p>
+ * Note: this interface should not be directly implemented. Use {@link AbstractDerivedStringValidator} instead.
+ *
+ * @param <R> string representation class
+ * @param <T> validated string representation class
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+public interface DerivedStringValidator<R extends DerivedString<R>, T extends R> {
+    /**
+     * Returns the instantiated representation class. The representation class is a {@link DerivedString} which
+     * understands the semantics of modeled data and has some internal representation of it. All {@link DerivedString}s
+     * which share the same representation class are considered equal if their internal state would result in the
+     * same canonical string representation as defined by the YANG data model.
+     *
+     * @return Representation {@link DerivedString} class.
+     */
+    Class<R> getRepresentationClass();
+
+    /**
+     * Return the class which captures the fact it was validated by this validator.
+     *
+     * @return Validated capture of the representation class.
+     */
+    Class<T> getValidatedRepresentationClass();
+
+    /**
+     * Validate a {@link DerivedString} representation. Implementations should override this method if they can
+     * provide a validation algorithm which does not rely on canonical strings but works on representation state only.
+     *
+     * @param value Representation value
+     * @return Validated representation
+     * @throws NullPointerException if {@code value} is null
+     * @throws IllegalArgumentException if the value does not meet validation criteria.
+     */
+    default T validateRepresentation(final R value) {
+        return validateRepresentation(value, value.toCanonicalString());
+    }
+
+    /**
+     * Validate a {@link DerivedString} representation. Implementations can chose whether they operate on representation
+     * state or canonical string -- both are considered equivalent. Users should call this method if they have
+     * a representation readily available.
+     *
+     * @param value Representation value
+     * @param canonicalString Canonical string matching the representation value
+     * @return Validated representation
+     * @throws NullPointerException if {@code value} or {@code canonicalString} is null.
+     * @throws IllegalArgumentException if the value does not meet validation criteria.
+     */
+    T validateRepresentation(R value, String canonicalString);
+}
diff --git a/yang/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/DerivedStringTest.java b/yang/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/DerivedStringTest.java
new file mode 100644 (file)
index 0000000..df27082
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.Test;
+
+@NonNullByDefault
+public class DerivedStringTest {
+    public static class EagerDerivedString extends CachingDerivedString<EagerDerivedString> {
+        private static final long serialVersionUID = 1L;
+
+        protected EagerDerivedString(final String str) {
+            super(str);
+        }
+
+        @Override
+        @SuppressWarnings("checkstyle:parameterName")
+        public final int compareTo(final EagerDerivedString o) {
+            return toCanonicalString().compareTo(o.toCanonicalString());
+        }
+
+        @Override
+        public final DerivedStringSupport<EagerDerivedString> support() {
+            return EAGER_SUPPORT;
+        }
+
+        @Override
+        public final int hashCode() {
+            return toCanonicalString().hashCode();
+        }
+
+        @Override
+        public final boolean equals(@Nullable final Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (obj instanceof String) {
+                return toCanonicalString().equals(obj);
+            }
+
+            return obj instanceof DerivedString
+                    && toCanonicalString().equals(((DerivedString<?>)obj).toCanonicalString());
+        }
+
+        @Override
+        protected final String computeCanonicalString() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    public static class LazyDerivedString extends CachingDerivedString<LazyDerivedString> {
+        private static final long serialVersionUID = 1L;
+
+        private final String str;
+
+        protected LazyDerivedString(final String str) {
+            this.str = str;
+        }
+
+        @Override
+        @SuppressWarnings("checkstyle:parameterName")
+        public final int compareTo(final LazyDerivedString o) {
+            return str.compareTo(o.str);
+        }
+
+        @Override
+        public final DerivedStringSupport<LazyDerivedString> support() {
+            return LAZY_SUPPORT;
+        }
+
+        @Override
+        public final int hashCode() {
+            return str.hashCode();
+        }
+
+        @Override
+        public final boolean equals(@Nullable final Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (obj instanceof String) {
+                return str.equals(obj);
+            }
+
+            return obj instanceof DerivedString && str.equals(((DerivedString<?>)obj).toCanonicalString());
+        }
+
+        @Override
+        protected final String computeCanonicalString() {
+            return str;
+        }
+    }
+
+    public static final class EagerDerivedStringSupport extends AbstractDerivedStringSupport<EagerDerivedString> {
+        EagerDerivedStringSupport() {
+            super(EagerDerivedString.class);
+        }
+
+        @Override
+        public EagerDerivedString fromString(final String str) {
+            return new EagerDerivedString(str);
+        }
+    }
+
+    public static final class LazyDerivedStringSupport extends AbstractDerivedStringSupport<LazyDerivedString> {
+        LazyDerivedStringSupport() {
+            super(LazyDerivedString.class);
+        }
+
+        @Override
+        public LazyDerivedString fromString(final String str) {
+            return new LazyDerivedString(str);
+        }
+    }
+
+    private static final DerivedStringSupport<EagerDerivedString> EAGER_SUPPORT = new EagerDerivedStringSupport();
+    private static final DerivedStringSupport<LazyDerivedString> LAZY_SUPPORT = new LazyDerivedStringSupport();
+
+    @Test
+    public void testEager() {
+        final DerivedString<?> foo = new EagerDerivedString("foo");
+        assertSame("foo", foo.toString());
+    }
+
+    @Test
+    public void testLazy() {
+        final DerivedString<?> foo = new LazyDerivedString("foo");
+        final String first = foo.toString();
+        assertEquals("foo", first);
+        assertSame(first, foo.toString());
+    }
+
+}