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