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

©2013 OpenDaylight, A Linux Foundation Collaborative Project. All Rights Reserved.
OpenDaylight is a registered trademark of The OpenDaylight Project, Inc.
Linux Foundation and OpenDaylight are registered trademarks of the Linux Foundation.
Linux is a registered trademark of Linus Torvalds.