From 6673a1b4c9c7dbd4274238561384542e93e8376b Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Sun, 1 Apr 2018 01:04:47 +0200 Subject: [PATCH] Add yang.common.DerivedString class 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 --- .../common/AbstractDerivedStringSupport.java | 70 +++++++ .../AbstractDerivedStringValidator.java | 89 ++++++++ .../yang/common/CachingDerivedString.java | 47 +++++ .../yangtools/yang/common/DerivedString.java | 192 ++++++++++++++++++ .../yang/common/DerivedStringSupport.java | 69 +++++++ .../yang/common/DerivedStringValidator.java | 70 +++++++ .../yang/common/DerivedStringTest.java | 143 +++++++++++++ 7 files changed, 680 insertions(+) create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringSupport.java create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringValidator.java create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/CachingDerivedString.java create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedString.java create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringSupport.java create mode 100644 yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringValidator.java create mode 100644 yang/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/DerivedStringTest.java 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 index 0000000000..56e4bac1ea --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringSupport.java @@ -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 derived string type + * @author Robert Varga + */ +@Beta +@NonNullByDefault +@ThreadSafe +public abstract class AbstractDerivedStringSupport> implements DerivedStringSupport { + private static final ClassValue VALIDATED_INSTANCES = new ClassValue() { + @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 representationClass; + + protected AbstractDerivedStringSupport(final Class representationClass) { + this.representationClass = DerivedString.validateRepresentationClass(representationClass); + VALIDATED_INSTANCES.get(getClass()); + } + + @Override + public final Class getRepresentationClass() { + return representationClass; + } + + @Override + public final Class 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 index 0000000000..40c98f9797 --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/AbstractDerivedStringValidator.java @@ -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 string representation class + * @param validated string representation class + * @author Robert Varga + */ +@Beta +@NonNullByDefault +@ThreadSafe +public abstract class AbstractDerivedStringValidator, T extends R> + implements DerivedStringValidator { + private final DerivedStringSupport representationSupport; + private final Class validatedClass; + + protected AbstractDerivedStringValidator(final DerivedStringSupport representationSupport, + final Class validatedClass) { + this.representationSupport = requireNonNull(representationSupport); + this.validatedClass = DerivedString.validateRepresentationClass(validatedClass); + } + + @Override + public final Class getRepresentationClass() { + return representationSupport.getRepresentationClass(); + } + + @Override + public final Class 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 index 0000000000..3a15c2a66e --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/CachingDerivedString.java @@ -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 derived string type + * @author Robert Varga + */ +@NonNullByDefault +public abstract class CachingDerivedString> extends DerivedString { + 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 index 0000000000..3dc1522439 --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedString.java @@ -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. + * + *

+ * 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}. + * + *

+ * Given the following YANG snippet: + *

+ *     typedef foo {
+ *         type string;
+ *         pattern "[1-9]?[0-9]";
+ *     }
+ *
+ *     typedef bar {
+ *         type foo;
+ *         patter "[1-9][0-9]";
+ *     }
+ *
+ *     typedef baz {
+ *         type foo;
+ *     }
+ * 
+ * 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: + *
    + *
  • {@code public class FooDerivedString extends DerivedString}, which implements all abstract + * methods of {@link DerivedString} as final methods. It will notably not override {@link #validator()} and + * must not be final.
  • + *
  • {@code public final class FooDerivedStringSupport extends DerivedStringSupport}, which + * forms the baseline validator and instantiation for {@code FooDerivedString}. It should be a singleton class + * with a getInstance() method.
  • + *
  • {@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}. + *
  • {@code public final class BarDerivedStringValidator extends DerivedStringValidator + *
+ * 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. + * + *

+ * 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 derived string representation + * @author Robert Varga + */ +@Beta +@NonNullByDefault +@ThreadSafe +public abstract class DerivedString> implements Comparable, Immutable, Serializable { + private static final class Validator extends ClassValue { + 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 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 support(); + + /** + * Return a {@link DerivedStringValidator} associated with this value's validated type. + * + * @return A {@link DerivedStringValidator} instance. + */ + public DerivedStringValidator validator() { + return support(); + } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(@Nullable Object obj); + + @Override + public final String toString() { + return toCanonicalString(); + } + + static > Class validateRepresentationClass(final Class 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 index 0000000000..56afaf63bf --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringSupport.java @@ -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. + * + *

+ * Note: never implement this interface directly, subclass {@link AbstractDerivedStringSupport} instead. + * + *

+ * 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 derived string representation + * @author Robert Varga + */ +@Beta +@NonNullByDefault +@ThreadSafe +public interface DerivedStringSupport> extends DerivedStringValidator { + /** + * 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 > DerivedStringSupport unsafe() { + return (DerivedStringSupport) 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 index 0000000000..e585328747 --- /dev/null +++ b/yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/DerivedStringValidator.java @@ -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. + * + *

+ * Note: this interface should not be directly implemented. Use {@link AbstractDerivedStringValidator} instead. + * + * @param string representation class + * @param validated string representation class + * @author Robert Varga + */ +@Beta +@NonNullByDefault +public interface DerivedStringValidator, 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 getRepresentationClass(); + + /** + * Return the class which captures the fact it was validated by this validator. + * + * @return Validated capture of the representation class. + */ + Class 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 index 0000000000..df270828ee --- /dev/null +++ b/yang/yang-common/src/test/java/org/opendaylight/yangtools/yang/common/DerivedStringTest.java @@ -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 { + 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 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 { + 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 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 { + EagerDerivedStringSupport() { + super(EagerDerivedString.class); + } + + @Override + public EagerDerivedString fromString(final String str) { + return new EagerDerivedString(str); + } + } + + public static final class LazyDerivedStringSupport extends AbstractDerivedStringSupport { + LazyDerivedStringSupport() { + super(LazyDerivedString.class); + } + + @Override + public LazyDerivedString fromString(final String str) { + return new LazyDerivedString(str); + } + } + + private static final DerivedStringSupport EAGER_SUPPORT = new EagerDerivedStringSupport(); + private static final DerivedStringSupport 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()); + } + +} -- 2.36.6