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