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