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