Fixup DerivedStringValidator representation checking
[yangtools.git] / yang / yang-common / src / main / java / org / opendaylight / yangtools / yang / common / DerivedString.java
1 /*
2  * Copyright (c) 2018 Pantheon Technologies, s.r.o. 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 com.google.common.base.Preconditions.checkArgument;
11
12 import com.google.common.annotations.Beta;
13 import java.io.Serializable;
14 import java.lang.reflect.Constructor;
15 import java.lang.reflect.Modifier;
16 import java.util.Arrays;
17 import javax.annotation.concurrent.ThreadSafe;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.concepts.Immutable;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24
25 /**
26  * Abstract base class for objects which are string-equivalent to canonical string representation specified
27  * in a YANG model. Note that each subclass of {@link DerivedString} defines its own {@link #hashCode()} and
28  * {@link #equals(Object)} contracts based on implementation particulars.
29  *
30  * <p>
31  * Since YANG validation works on top of strings, which in itself is expensive and this class provides storage which
32  * is potentially not based on strings, its design combines 'representation' and 'validated to match constraints'
33  * aspects of a YANG type derived from string. To achieve that it cooperates with {@link DerivedStringValidator} and
34  * {@link DerivedStringSupport}.
35  *
36  * <p>
37  * Given the following YANG snippet:
38  * <pre>
39  *     typedef foo {
40  *         type string;
41  *         pattern "[1-9]?[0-9]";
42  *     }
43  *
44  *     typedef bar {
45  *         type foo;
46  *         patter "[1-9][0-9]";
47  *     }
48  *
49  *     typedef baz {
50  *         type foo;
51  *     }
52  * </pre>
53  * it is obvious we could use a storage class with 'int' as the internal representation of all three types and define
54  * operations on top of it. In this case we would define:
55  * <ul>
56  *     <li>{@code public class FooDerivedString extends DerivedString<FooDerivedString>}, which implements all abstract
57  *         methods of {@link DerivedString} as final methods. It will notably not override {@link #validator()} and
58  *         must not be final.</li>
59  *     <li>{@code public final class FooDerivedStringSupport extends DerivedStringSupport<FooDerivedString>}, which
60  *         forms the baseline validator and instantiation for {@code FooDerivedString}. It should be a singleton class
61  *         with a getInstance() method.</li>
62  *     <li>{@code public class BarDerivedString extends FooDerivedString}, which overrides {@link #validator()} to
63  *         indicate its contents have been validated to conform to bar -- it does that by returning the singleton
64  *         instance of {@code BarDerivedStringValidator}.
65  *     <li>{@code public final class BarDerivedStringValidator extends DerivedStringValidator<FooDerivedString,
66  *         BarDerivedString}. This method needs to notably implement
67  *         {@link DerivedStringValidator#validateRepresentation(DerivedString)} to hand out BarDerivedString instances.
68  *         This class needs to be a singleton with a getInstance() method, too.</li>
69  * </ul>
70  * Since {@code baz} is not defining any new restrictions, all instances of FooDerivedString are valid for it and we
71  * do not have to define any additional support.
72  *
73  * <p>
74  * It is important for {@link DerivedString} subclasses not to be final because any YANG type can be further extended
75  * and adding a final class in that hierarchy would prevent a proper class from being defined.
76  *
77  * @param <R> derived string representation
78  * @author Robert Varga
79  */
80 @Beta
81 @NonNullByDefault
82 @ThreadSafe
83 public abstract class DerivedString<R extends DerivedString<R>> implements Comparable<R>, Immutable, Serializable {
84     private abstract static class AbstractValidator extends ClassValue<Boolean> {
85         private static final Logger LOG = LoggerFactory.getLogger(AbstractValidator.class);
86
87         @Override
88         protected final Boolean computeValue(final @Nullable Class<?> type) {
89             // Every DerivedString representation class must:
90             checkArgument(DerivedString.class.isAssignableFrom(type), "%s is not a DerivedString", type);
91
92             // be non-final and public
93             final int modifiers = type.getModifiers();
94             checkArgument(Modifier.isPublic(modifiers), "%s must be public", type);
95             checkArgument(!Modifier.isFinal(modifiers), "%s must not be final", type);
96
97             // have at least one public or protected constructor (for subclasses)
98             checkArgument(Arrays.stream(type.getDeclaredConstructors()).mapToInt(Constructor::getModifiers)
99                 .anyMatch(mod -> Modifier.isProtected(mod) || Modifier.isPublic(mod)),
100                 "%s must declare at least one protected or public constructor", type);
101
102             try {
103                 // have a non-final non-abstract validator() method
104                 final int validator;
105                 try {
106                     validator = type.getMethod("validator").getModifiers();
107                 } catch (NoSuchMethodException e) {
108                     throw new IllegalArgumentException(type + " must have a non-abstract non-final validator() method",
109                         e);
110                 }
111                 checkArgument(!Modifier.isFinal(validator), "%s must not have final validator()", type);
112
113                 // have final toCanonicalString(), support(), hashCode() and equals(Object), compare(T) methods
114                 checkFinalMethod(type, "toCanonicalString");
115                 checkFinalMethod(type, "support");
116                 checkFinalMethod(type, "hashCode");
117                 checkFinalMethod(type, "equals", Object.class);
118             } catch (SecurityException e) {
119                 LOG.warn("Cannot completely validate {}", type, e);
120                 return Boolean.FALSE;
121             }
122
123             return Boolean.TRUE;
124         }
125
126         abstract void checkCompareTo(Class<?> type);
127
128         private static void checkFinalMethod(final Class<?> type, final String name) {
129             try {
130                 checkFinalMethod(type.getMethod(name).getModifiers(), type, name, "");
131             } catch (NoSuchMethodException e) {
132                 throw new IllegalArgumentException(type + " must have a final " + name + "() method", e);
133             }
134         }
135
136         static void checkFinalMethod(final Class<?> type, final String name, final Class<?> arg) {
137             final String argName = arg.getSimpleName();
138             try {
139                 checkFinalMethod(type.getMethod(name, arg).getModifiers(), type, name, argName);
140             } catch (NoSuchMethodException e) {
141                 throw new IllegalArgumentException(type + " must have a final " + name + "(" + argName + ") method", e);
142             }
143         }
144
145         private static void checkFinalMethod(final int modifiers, final Class<?> type, final String name,
146                 final String args) {
147             checkArgument(Modifier.isFinal(modifiers), "%s must have a final %s(%s) method", type, name, args);
148         }
149     }
150
151     private static final ClassValue<Boolean> VALIDATED_REPRESENTATIONS = new AbstractValidator() {
152         @Override
153         void checkCompareTo(@NonNull final Class<?> type) {
154             checkFinalMethod(type, "compareTo", type);
155         }
156     };
157     private static final ClassValue<Boolean> VALIDATED_VALIDATIONS = new AbstractValidator() {
158         @Override
159         void checkCompareTo(@NonNull final Class<?> type) {
160             // Intentional no-op, as we'd need a type capture of the representation
161         }
162     };
163     private static final long serialVersionUID = 1L;
164
165     /**
166      * Return the canonical string representation of this object's value.
167      *
168      * @return Canonical string
169      */
170     public abstract String toCanonicalString();
171
172     /**
173      * Return the {@link DerivedStringSupport} associated with this type. It can be used to create new instances of this
174      * representation.
175      *
176      * @return A {@link DerivedStringSupport} instance.
177      */
178     public abstract DerivedStringSupport<R> support();
179
180     /**
181      * Return a {@link DerivedStringValidator} associated with this value's validated type.
182      *
183      * @return A {@link DerivedStringValidator} instance.
184      */
185     public DerivedStringValidator<R, ? extends R> validator() {
186         return support();
187     }
188
189     @Override
190     public abstract int hashCode();
191
192     @Override
193     public abstract boolean equals(@Nullable Object obj);
194
195     @Override
196     public final String toString() {
197         return toCanonicalString();
198     }
199
200     static <T extends DerivedString<?>> Class<T> validateRepresentationClass(final Class<T> representationClass) {
201         // Validation is reflective, cache its result
202         VALIDATED_REPRESENTATIONS.get(representationClass);
203         return representationClass;
204     }
205
206     static <T extends DerivedString<?>> Class<T> validateValidationClass(final Class<T> representationClass) {
207         // Validation is reflective, cache its result
208         VALIDATED_VALIDATIONS.get(representationClass);
209         return representationClass;
210     }
211 }