--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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());
+ }
+
+}