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