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