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