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