Do not implement concepts.Builder
[controller.git] / opendaylight / md-sal / sal-distributed-datastore / src / main / java / org / opendaylight / controller / cluster / datastore / DatastoreContextIntrospector.java
1 /*
2  * Copyright (c) 2015 Brocade Communications Systems, Inc. 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.controller.cluster.datastore;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import com.google.common.collect.ImmutableMap;
13 import com.google.common.collect.ImmutableSet;
14 import com.google.common.primitives.Primitives;
15 import java.lang.reflect.Constructor;
16 import java.lang.reflect.InvocationTargetException;
17 import java.lang.reflect.Method;
18 import java.util.AbstractMap.SimpleImmutableEntry;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Optional;
27 import java.util.Set;
28 import java.util.function.Function;
29 import javax.management.ConstructorParameters;
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.commons.text.WordUtils;
32 import org.checkerframework.checker.lock.qual.GuardedBy;
33 import org.opendaylight.controller.cluster.datastore.DatastoreContext.Builder;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.config.distributed.datastore.provider.rev140612.DataStoreProperties;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.config.distributed.datastore.provider.rev140612.DataStorePropertiesContainer;
36 import org.opendaylight.yangtools.yang.common.Uint16;
37 import org.opendaylight.yangtools.yang.common.Uint32;
38 import org.opendaylight.yangtools.yang.common.Uint64;
39 import org.opendaylight.yangtools.yang.common.Uint8;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 /**
44  * Introspects on a DatastoreContext instance to set its properties via reflection.
45  * i
46  * @author Thomas Pantelis
47  */
48 public class DatastoreContextIntrospector {
49     private static final Logger LOG = LoggerFactory.getLogger(DatastoreContextIntrospector.class);
50
51     private static final Map<String, Entry<Class<?>, Method>> DATA_STORE_PROP_INFO = new HashMap<>();
52
53     private static final Map<Class<?>, Constructor<?>> CONSTRUCTORS = new HashMap<>();
54
55     private static final Map<Class<?>, Method> YANG_TYPE_GETTERS = new HashMap<>();
56
57     private static final Map<String, Method> BUILDER_SETTERS = new HashMap<>();
58
59     private static final ImmutableMap<Class<?>, Function<String, Object>> UINT_FACTORIES =
60             ImmutableMap.<Class<?>, Function<String, Object>>builder()
61             .put(Uint8.class, Uint8::valueOf)
62             .put(Uint16.class, Uint16::valueOf)
63             .put(Uint32.class, Uint32::valueOf)
64             .put(Uint64.class, Uint64::valueOf)
65             .build();
66
67     static {
68         try {
69             introspectDatastoreContextBuilder();
70             introspectDataStoreProperties();
71             introspectPrimitiveTypes();
72         } catch (final IllegalArgumentException e) {
73             LOG.error("Error initializing DatastoreContextIntrospector", e);
74         }
75     }
76
77     /**
78      * Introspects each primitive wrapper (ie Integer, Long etc) and String type to find the
79      * constructor that takes a single String argument. For primitive wrappers, this constructor
80      * converts from a String representation.
81      */
82     // Disables "Either log or rethrow this exception" sonar warning
83     @SuppressWarnings("squid:S1166")
84     private static void introspectPrimitiveTypes() {
85         final Set<Class<?>> primitives = ImmutableSet.<Class<?>>builder().addAll(
86                 Primitives.allWrapperTypes()).add(String.class).build();
87         for (final Class<?> primitive: primitives) {
88             try {
89                 processPropertyType(primitive);
90             } catch (final NoSuchMethodException e) {
91                 // Ignore primitives that can't be constructed from a String, eg Character and Void.
92             } catch (SecurityException | IllegalArgumentException e) {
93                 LOG.error("Error introspect primitive type {}", primitive, e);
94             }
95         }
96     }
97
98     /**
99      * Introspects the DatastoreContext.Builder class to find all its setter methods that we will
100      * invoke via reflection. We can't use the bean Introspector here as the Builder setters don't
101      * follow the bean property naming convention, ie setter prefixed with "set", so look for all
102      * the methods that return Builder.
103      */
104     private static void introspectDatastoreContextBuilder() {
105         for (final Method method: Builder.class.getMethods()) {
106             if (Builder.class.equals(method.getReturnType())) {
107                 BUILDER_SETTERS.put(method.getName(), method);
108             }
109         }
110     }
111
112     /**
113      * Introspects the DataStoreProperties interface that is generated from the DataStoreProperties
114      * yang grouping. We use the bean Introspector to find the types of all the properties defined
115      * in the interface (this is the type returned from the getter method). For each type, we find
116      * the appropriate constructor that we will use.
117      *
118      * @throws IllegalArgumentException if failed to process yang-defined property
119      */
120     private static void introspectDataStoreProperties() {
121         for (final Method method : DataStoreProperties.class.getDeclaredMethods()) {
122             final String propertyName = getPropertyName(method);
123             if (propertyName != null) {
124                 processDataStoreProperty(propertyName, method.getReturnType(), method);
125             }
126         }
127     }
128
129     private static String getPropertyName(final Method method) {
130         final String methodName = method.getName();
131         if (Boolean.class.equals(method.getReturnType()) && methodName.startsWith("is")) {
132             return WordUtils.uncapitalize(methodName.substring(2));
133         } else if (methodName.startsWith("get")) {
134             return WordUtils.uncapitalize(methodName.substring(3));
135         }
136         return null;
137     }
138
139     /**
140      * Processes a property defined on the DataStoreProperties interface.
141      */
142     @SuppressWarnings("checkstyle:IllegalCatch")
143     private static void processDataStoreProperty(final String name, final Class<?> propertyType,
144             final Method readMethod) {
145         checkArgument(BUILDER_SETTERS.containsKey(name),
146                 "DataStoreProperties property \"%s\" does not have corresponding setter in DatastoreContext.Builder",
147                 name);
148         try {
149             processPropertyType(propertyType);
150             DATA_STORE_PROP_INFO.put(name, new SimpleImmutableEntry<>(propertyType, readMethod));
151         } catch (final Exception e) {
152             LOG.error("Error finding constructor for type {}", propertyType, e);
153         }
154     }
155
156     /**
157      * Finds the appropriate constructor for the specified type that we will use to construct
158      * instances.
159      *
160      * @throws IllegalArgumentException if yang-defined type has no property, annotated by ConstructorParameters
161      */
162     private static void processPropertyType(final Class<?> propertyType)
163             throws NoSuchMethodException, SecurityException {
164         final Class<?> wrappedType = Primitives.wrap(propertyType);
165         if (CONSTRUCTORS.containsKey(wrappedType)) {
166             return;
167         }
168
169         // If the type is a primitive (or String type), we look for the constructor that takes a
170         // single String argument, which, for primitives, validates and converts from a String
171         // representation which is the form we get on ingress.
172         if (propertyType.isPrimitive() || Primitives.isWrapperType(propertyType) || propertyType.equals(String.class)) {
173             CONSTRUCTORS.put(wrappedType, propertyType.getConstructor(String.class));
174         } else {
175             // This must be a yang-defined type. We need to find the constructor that takes a
176             // primitive as the only argument. This will be used to construct instances to perform
177             // validation (eg range checking). The yang-generated types have a couple single-argument
178             // constructors but the one we want has the bean ConstructorProperties annotation.
179             for (final Constructor<?> ctor: propertyType.getConstructors()) {
180                 final ConstructorParameters ctorParAnnotation = ctor.getAnnotation(ConstructorParameters.class);
181                 if (ctor.getParameterCount() == 1 && ctorParAnnotation != null) {
182                     findYangTypeGetter(propertyType, ctorParAnnotation.value()[0]);
183                     CONSTRUCTORS.put(propertyType, ctor);
184                     break;
185                 }
186             }
187         }
188     }
189
190     /**
191      * Finds the getter method on a yang-generated type for the specified property name.
192      *
193      * @throws IllegalArgumentException if passed type has no passed property
194      */
195     private static void findYangTypeGetter(final Class<?> type, final String propertyName) {
196         for (Method method : type.getDeclaredMethods()) {
197             final String property = getPropertyName(method);
198             if (property != null && property.equals(propertyName)) {
199                 YANG_TYPE_GETTERS.put(type, method);
200                 return;
201             }
202         }
203
204         throw new IllegalArgumentException(String.format(
205                 "Getter method for constructor property %s not found for YANG type %s",
206                 propertyName, type));
207     }
208
209     @GuardedBy(value = "this")
210     private DatastoreContext context;
211     @GuardedBy(value = "this")
212     private Map<String, Object> currentProperties;
213
214     public DatastoreContextIntrospector(final DatastoreContext context,
215             final DataStorePropertiesContainer defaultPropsContainer) {
216
217         final Builder builder = DatastoreContext.newBuilderFrom(context);
218         for (Entry<String, Entry<Class<?>, Method>> entry: DATA_STORE_PROP_INFO.entrySet()) {
219             Object value;
220             try {
221                 value = entry.getValue().getValue().invoke(defaultPropsContainer);
222             } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
223                 LOG.error("Error obtaining default value for property {}", entry.getKey(), e);
224                 value = null;
225             }
226
227             if (value != null) {
228                 convertValueAndInvokeSetter(entry.getKey(), value, builder);
229             }
230         }
231
232         this.context = builder.build();
233     }
234
235     public synchronized DatastoreContext getContext() {
236         return context;
237     }
238
239     public DatastoreContextFactory newContextFactory() {
240         return new DatastoreContextFactory(this);
241     }
242
243     public synchronized DatastoreContext getShardDatastoreContext(final String forShardName) {
244         if (currentProperties == null) {
245             return context;
246         }
247
248         final Builder builder = DatastoreContext.newBuilderFrom(context);
249         final String dataStoreTypePrefix = context.getDataStoreName() + '.';
250         final String shardNamePrefix = forShardName + '.';
251
252         final List<String> keys = getSortedKeysByDatastoreType(currentProperties.keySet(), dataStoreTypePrefix);
253
254         for (String key: keys) {
255             final Object value = currentProperties.get(key);
256             if (key.startsWith(dataStoreTypePrefix)) {
257                 key = key.replaceFirst(dataStoreTypePrefix, "");
258             }
259
260             if (key.startsWith(shardNamePrefix)) {
261                 key = key.replaceFirst(shardNamePrefix, "");
262                 convertValueAndInvokeSetter(key, value.toString(), builder);
263             }
264         }
265
266         return builder.build();
267     }
268
269     /**
270      * Applies the given properties to the cached DatastoreContext and yields a new DatastoreContext
271      * instance which can be obtained via {@link #getContext()}.
272      *
273      * @param properties the properties to apply
274      * @return true if the cached DatastoreContext was updated, false otherwise.
275      */
276     public synchronized boolean update(final Map<String, Object> properties) {
277         currentProperties = null;
278         if (properties == null || properties.isEmpty()) {
279             return false;
280         }
281
282         LOG.debug("In update: properties: {}", properties);
283
284         final ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.<String, Object>builder();
285
286         final Builder builder = DatastoreContext.newBuilderFrom(context);
287
288         final String dataStoreTypePrefix = context.getDataStoreName() + '.';
289
290         final List<String> keys = getSortedKeysByDatastoreType(properties.keySet(), dataStoreTypePrefix);
291
292         boolean updated = false;
293         for (String key: keys) {
294             final Object value = properties.get(key);
295             mapBuilder.put(key, value);
296
297             // If the key is prefixed with the data store type, strip it off.
298             if (key.startsWith(dataStoreTypePrefix)) {
299                 key = key.replaceFirst(dataStoreTypePrefix, "");
300             }
301
302             if (convertValueAndInvokeSetter(key, value.toString(), builder)) {
303                 updated = true;
304             }
305         }
306
307         currentProperties = mapBuilder.build();
308
309         if (updated) {
310             context = builder.build();
311         }
312
313         return updated;
314     }
315
316     private static ArrayList<String> getSortedKeysByDatastoreType(final Collection<String> inKeys,
317             final String dataStoreTypePrefix) {
318         // Sort the property keys by putting the names prefixed with the data store type last. This
319         // is done so data store specific settings are applied after global settings.
320         final ArrayList<String> keys = new ArrayList<>(inKeys);
321         keys.sort((key1, key2) -> key1.startsWith(dataStoreTypePrefix) ? 1 :
322             key2.startsWith(dataStoreTypePrefix) ? -1 : key1.compareTo(key2));
323         return keys;
324     }
325
326     @SuppressWarnings("checkstyle:IllegalCatch")
327     private boolean convertValueAndInvokeSetter(final String inKey, final Object inValue, final Builder builder) {
328         final String key = convertToCamelCase(inKey);
329
330         try {
331             // Convert the value to the right type.
332             final Object value = convertValue(key, inValue);
333             if (value == null) {
334                 return false;
335             }
336
337             LOG.debug("Converted value for property {}: {} ({})",
338                     key, value, value.getClass().getSimpleName());
339
340             // Call the setter method on the Builder instance.
341             final Method setter = BUILDER_SETTERS.get(key);
342             if (value.getClass().isEnum()) {
343                 setter.invoke(builder, value);
344             } else {
345                 setter.invoke(builder, constructorValueRecursively(
346                         Primitives.wrap(setter.getParameterTypes()[0]), value.toString()));
347             }
348
349             return true;
350         } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
351                 | InstantiationException e) {
352             LOG.error("Error converting value ({}) for property {}", inValue, key, e);
353         }
354
355         return false;
356     }
357
358     private static String convertToCamelCase(final String inString) {
359         String str = inString.trim();
360         if (StringUtils.contains(str, '-') || StringUtils.contains(str, ' ')) {
361             str = inString.replace('-', ' ');
362             str = WordUtils.capitalizeFully(str);
363             str = StringUtils.deleteWhitespace(str);
364         }
365
366         return StringUtils.uncapitalize(str);
367     }
368
369     private Object convertValue(final String name, final Object from)
370             throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
371         final Entry<Class<?>, Method> propertyInfo = DATA_STORE_PROP_INFO.get(name);
372         if (propertyInfo == null) {
373             LOG.debug("Property not found for {}", name);
374             return null;
375         }
376
377         final Class<?> propertyType = propertyInfo.getKey();
378
379         LOG.debug("Type for property {}: {}, converting value {} ({})",
380                 name, propertyType.getSimpleName(), from, from.getClass().getSimpleName());
381
382         if (propertyType.isEnum()) {
383             try {
384                 final Method enumConstructor = propertyType.getDeclaredMethod("forName", String.class);
385                 final Object optional =  enumConstructor.invoke(null, from.toString().toLowerCase(Locale.ROOT));
386                 if (optional instanceof Optional) {
387                     return ((Optional<Object>)optional).orElseThrow();
388                 }
389             } catch (NoSuchMethodException e) {
390                 LOG.error("Error constructing value ({}) for enum {}", from, propertyType);
391             }
392         }
393
394         // Recurse the chain of constructors depth-first to get the resulting value. Eg, if the
395         // property type is the yang-generated NonZeroUint32Type, it's constructor takes a Long so
396         // we have to first construct a Long instance from the input value.
397         Object converted = constructorValueRecursively(propertyType, from);
398
399         // If the converted type is a yang-generated type, call the getter to obtain the actual value.
400         final Method getter = YANG_TYPE_GETTERS.get(converted.getClass());
401         if (getter != null) {
402             converted = getter.invoke(converted);
403         }
404
405         return converted;
406     }
407
408     private Object constructorValueRecursively(final Class<?> toType, final Object fromValue)
409             throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
410         LOG.trace("convertValueRecursively - toType: {}, fromValue {} ({})",
411                 toType.getSimpleName(), fromValue, fromValue.getClass().getSimpleName());
412
413         if (toType.equals(fromValue.getClass())) {
414             return fromValue;
415         }
416
417         final Constructor<?> ctor = CONSTRUCTORS.get(toType);
418         if (ctor == null) {
419             if (fromValue instanceof String) {
420                 final Function<String, Object> factory = UINT_FACTORIES.get(toType);
421                 if (factory != null) {
422                     return factory.apply((String) fromValue);
423                 }
424             }
425
426             throw new IllegalArgumentException(String.format("Constructor not found for type %s", toType));
427         }
428
429         LOG.trace("Found {}", ctor);
430         Object value = fromValue;
431
432         // Once we find a constructor that takes the original type as an argument, we're done recursing.
433         if (!ctor.getParameterTypes()[0].equals(fromValue.getClass())) {
434             value = constructorValueRecursively(ctor.getParameterTypes()[0], fromValue);
435         }
436
437         return ctor.newInstance(value);
438     }
439 }