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