2 * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.controller.cluster.datastore;
10 import static com.google.common.base.Preconditions.checkArgument;
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.Locale;
25 import java.util.Map.Entry;
26 import java.util.Optional;
28 import java.util.function.Function;
29 import javax.management.ConstructorParameters;
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.commons.text.WordUtils;
32 import org.checkerframework.checker.lock.qual.GuardedBy;
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.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.config.distributed.datastore.provider.rev140612.DataStorePropertiesContainer;
36 import org.opendaylight.yangtools.yang.common.Uint16;
37 import org.opendaylight.yangtools.yang.common.Uint32;
38 import org.opendaylight.yangtools.yang.common.Uint64;
39 import org.opendaylight.yangtools.yang.common.Uint8;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * Introspects on a DatastoreContext instance to set its properties via reflection.
46 * @author Thomas Pantelis
48 public class DatastoreContextIntrospector {
49 private static final Logger LOG = LoggerFactory.getLogger(DatastoreContextIntrospector.class);
51 private static final Map<String, Entry<Class<?>, Method>> DATA_STORE_PROP_INFO = new HashMap<>();
53 private static final Map<Class<?>, Constructor<?>> CONSTRUCTORS = new HashMap<>();
55 private static final Map<Class<?>, Method> YANG_TYPE_GETTERS = new HashMap<>();
57 private static final Map<String, Method> BUILDER_SETTERS = new HashMap<>();
59 private static final ImmutableMap<Class<?>, Function<String, Object>> UINT_FACTORIES =
60 ImmutableMap.<Class<?>, Function<String, Object>>builder()
61 .put(Uint8.class, Uint8::valueOf)
62 .put(Uint16.class, Uint16::valueOf)
63 .put(Uint32.class, Uint32::valueOf)
64 .put(Uint64.class, Uint64::valueOf)
69 introspectDatastoreContextBuilder();
70 introspectDataStoreProperties();
71 introspectPrimitiveTypes();
72 } catch (final IllegalArgumentException e) {
73 LOG.error("Error initializing DatastoreContextIntrospector", e);
78 * Introspects each primitive wrapper (ie Integer, Long etc) and String type to find the
79 * constructor that takes a single String argument. For primitive wrappers, this constructor
80 * converts from a String representation.
82 // Disables "Either log or rethrow this exception" sonar warning
83 @SuppressWarnings("squid:S1166")
84 private static void introspectPrimitiveTypes() {
85 final Set<Class<?>> primitives = ImmutableSet.<Class<?>>builder().addAll(
86 Primitives.allWrapperTypes()).add(String.class).build();
87 for (final Class<?> primitive: primitives) {
89 processPropertyType(primitive);
90 } catch (final NoSuchMethodException e) {
91 // Ignore primitives that can't be constructed from a String, eg Character and Void.
92 } catch (SecurityException | IllegalArgumentException e) {
93 LOG.error("Error introspect primitive type {}", primitive, e);
99 * Introspects the DatastoreContext.Builder class to find all its setter methods that we will
100 * invoke via reflection. We can't use the bean Introspector here as the Builder setters don't
101 * follow the bean property naming convention, ie setter prefixed with "set", so look for all
102 * the methods that return Builder.
104 private static void introspectDatastoreContextBuilder() {
105 for (final Method method: Builder.class.getMethods()) {
106 if (Builder.class.equals(method.getReturnType())) {
107 BUILDER_SETTERS.put(method.getName(), method);
113 * Introspects the DataStoreProperties interface that is generated from the DataStoreProperties
114 * yang grouping. We use the bean Introspector to find the types of all the properties defined
115 * in the interface (this is the type returned from the getter method). For each type, we find
116 * the appropriate constructor that we will use.
118 * @throws IllegalArgumentException if failed to process yang-defined property
120 private static void introspectDataStoreProperties() {
121 for (final Method method : DataStoreProperties.class.getDeclaredMethods()) {
122 final String propertyName = getPropertyName(method);
123 if (propertyName != null) {
124 processDataStoreProperty(propertyName, method.getReturnType(), method);
129 private static String getPropertyName(final Method method) {
130 final String methodName = method.getName();
131 if (Boolean.class.equals(method.getReturnType()) && methodName.startsWith("is")) {
132 return WordUtils.uncapitalize(methodName.substring(2));
133 } else if (methodName.startsWith("get")) {
134 return WordUtils.uncapitalize(methodName.substring(3));
140 * Processes a property defined on the DataStoreProperties interface.
142 @SuppressWarnings("checkstyle:IllegalCatch")
143 private static void processDataStoreProperty(final String name, final Class<?> propertyType,
144 final Method readMethod) {
145 checkArgument(BUILDER_SETTERS.containsKey(name),
146 "DataStoreProperties property \"%s\" does not have corresponding setter in DatastoreContext.Builder",
149 processPropertyType(propertyType);
150 DATA_STORE_PROP_INFO.put(name, new SimpleImmutableEntry<>(propertyType, readMethod));
151 } catch (final Exception e) {
152 LOG.error("Error finding constructor for type {}", propertyType, e);
157 * Finds the appropriate constructor for the specified type that we will use to construct
160 * @throws IllegalArgumentException if yang-defined type has no property, annotated by ConstructorParameters
162 private static void processPropertyType(final Class<?> propertyType)
163 throws NoSuchMethodException, SecurityException {
164 final Class<?> wrappedType = Primitives.wrap(propertyType);
165 if (CONSTRUCTORS.containsKey(wrappedType)) {
169 // If the type is a primitive (or String type), we look for the constructor that takes a
170 // single String argument, which, for primitives, validates and converts from a String
171 // representation which is the form we get on ingress.
172 if (propertyType.isPrimitive() || Primitives.isWrapperType(propertyType) || propertyType.equals(String.class)) {
173 CONSTRUCTORS.put(wrappedType, propertyType.getConstructor(String.class));
175 // This must be a yang-defined type. We need to find the constructor that takes a
176 // primitive as the only argument. This will be used to construct instances to perform
177 // validation (eg range checking). The yang-generated types have a couple single-argument
178 // constructors but the one we want has the bean ConstructorProperties annotation.
179 for (final Constructor<?> ctor: propertyType.getConstructors()) {
180 final ConstructorParameters ctorParAnnotation = ctor.getAnnotation(ConstructorParameters.class);
181 if (ctor.getParameterCount() == 1 && ctorParAnnotation != null) {
182 findYangTypeGetter(propertyType, ctorParAnnotation.value()[0]);
183 CONSTRUCTORS.put(propertyType, ctor);
191 * Finds the getter method on a yang-generated type for the specified property name.
193 * @throws IllegalArgumentException if passed type has no passed property
195 private static void findYangTypeGetter(final Class<?> type, final String propertyName) {
196 for (Method method : type.getDeclaredMethods()) {
197 final String property = getPropertyName(method);
198 if (property != null && property.equals(propertyName)) {
199 YANG_TYPE_GETTERS.put(type, method);
204 throw new IllegalArgumentException(String.format(
205 "Getter method for constructor property %s not found for YANG type %s",
206 propertyName, type));
209 @GuardedBy(value = "this")
210 private DatastoreContext context;
211 @GuardedBy(value = "this")
212 private Map<String, Object> currentProperties;
214 public DatastoreContextIntrospector(final DatastoreContext context,
215 final DataStorePropertiesContainer defaultPropsContainer) {
217 final Builder builder = DatastoreContext.newBuilderFrom(context);
218 for (Entry<String, Entry<Class<?>, Method>> entry: DATA_STORE_PROP_INFO.entrySet()) {
221 value = entry.getValue().getValue().invoke(defaultPropsContainer);
222 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
223 LOG.error("Error obtaining default value for property {}", entry.getKey(), e);
228 convertValueAndInvokeSetter(entry.getKey(), value, builder);
232 this.context = builder.build();
235 public synchronized DatastoreContext getContext() {
239 public DatastoreContextFactory newContextFactory() {
240 return new DatastoreContextFactory(this);
243 public synchronized DatastoreContext getShardDatastoreContext(final String forShardName) {
244 if (currentProperties == null) {
248 final Builder builder = DatastoreContext.newBuilderFrom(context);
249 final String dataStoreTypePrefix = context.getDataStoreName() + '.';
250 final String shardNamePrefix = forShardName + '.';
252 final List<String> keys = getSortedKeysByDatastoreType(currentProperties.keySet(), dataStoreTypePrefix);
254 for (String key: keys) {
255 final Object value = currentProperties.get(key);
256 if (key.startsWith(dataStoreTypePrefix)) {
257 key = key.replaceFirst(dataStoreTypePrefix, "");
260 if (key.startsWith(shardNamePrefix)) {
261 key = key.replaceFirst(shardNamePrefix, "");
262 convertValueAndInvokeSetter(key, value.toString(), builder);
266 return builder.build();
270 * Applies the given properties to the cached DatastoreContext and yields a new DatastoreContext
271 * instance which can be obtained via {@link #getContext()}.
273 * @param properties the properties to apply
274 * @return true if the cached DatastoreContext was updated, false otherwise.
276 public synchronized boolean update(final Map<String, Object> properties) {
277 currentProperties = null;
278 if (properties == null || properties.isEmpty()) {
282 LOG.debug("In update: properties: {}", properties);
284 final ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.<String, Object>builder();
286 final Builder builder = DatastoreContext.newBuilderFrom(context);
288 final String dataStoreTypePrefix = context.getDataStoreName() + '.';
290 final List<String> keys = getSortedKeysByDatastoreType(properties.keySet(), dataStoreTypePrefix);
292 boolean updated = false;
293 for (String key: keys) {
294 final Object value = properties.get(key);
295 mapBuilder.put(key, value);
297 // If the key is prefixed with the data store type, strip it off.
298 if (key.startsWith(dataStoreTypePrefix)) {
299 key = key.replaceFirst(dataStoreTypePrefix, "");
302 if (convertValueAndInvokeSetter(key, value.toString(), builder)) {
307 currentProperties = mapBuilder.build();
310 context = builder.build();
316 private static ArrayList<String> getSortedKeysByDatastoreType(final Collection<String> inKeys,
317 final String dataStoreTypePrefix) {
318 // Sort the property keys by putting the names prefixed with the data store type last. This
319 // is done so data store specific settings are applied after global settings.
320 final ArrayList<String> keys = new ArrayList<>(inKeys);
321 keys.sort((key1, key2) -> key1.startsWith(dataStoreTypePrefix) ? 1 :
322 key2.startsWith(dataStoreTypePrefix) ? -1 : key1.compareTo(key2));
326 @SuppressWarnings("checkstyle:IllegalCatch")
327 private boolean convertValueAndInvokeSetter(final String inKey, final Object inValue, final Builder builder) {
328 final String key = convertToCamelCase(inKey);
331 // Convert the value to the right type.
332 final Object value = convertValue(key, inValue);
337 LOG.debug("Converted value for property {}: {} ({})",
338 key, value, value.getClass().getSimpleName());
340 // Call the setter method on the Builder instance.
341 final Method setter = BUILDER_SETTERS.get(key);
342 if (value.getClass().isEnum()) {
343 setter.invoke(builder, value);
345 setter.invoke(builder, constructorValueRecursively(
346 Primitives.wrap(setter.getParameterTypes()[0]), value.toString()));
350 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
351 | InstantiationException e) {
352 LOG.error("Error converting value ({}) for property {}", inValue, key, e);
358 private static String convertToCamelCase(final String inString) {
359 String str = inString.trim();
360 if (StringUtils.contains(str, '-') || StringUtils.contains(str, ' ')) {
361 str = inString.replace('-', ' ');
362 str = WordUtils.capitalizeFully(str);
363 str = StringUtils.deleteWhitespace(str);
366 return StringUtils.uncapitalize(str);
369 private Object convertValue(final String name, final Object from)
370 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
371 final Entry<Class<?>, Method> propertyInfo = DATA_STORE_PROP_INFO.get(name);
372 if (propertyInfo == null) {
373 LOG.debug("Property not found for {}", name);
377 final Class<?> propertyType = propertyInfo.getKey();
379 LOG.debug("Type for property {}: {}, converting value {} ({})",
380 name, propertyType.getSimpleName(), from, from.getClass().getSimpleName());
382 if (propertyType.isEnum()) {
384 final Method enumConstructor = propertyType.getDeclaredMethod("forName", String.class);
385 final Object optional = enumConstructor.invoke(null, from.toString().toLowerCase(Locale.ROOT));
386 if (optional instanceof Optional) {
387 return ((Optional<Object>)optional).orElseThrow();
389 } catch (NoSuchMethodException e) {
390 LOG.error("Error constructing value ({}) for enum {}", from, propertyType);
394 // Recurse the chain of constructors depth-first to get the resulting value. Eg, if the
395 // property type is the yang-generated NonZeroUint32Type, it's constructor takes a Long so
396 // we have to first construct a Long instance from the input value.
397 Object converted = constructorValueRecursively(propertyType, from);
399 // If the converted type is a yang-generated type, call the getter to obtain the actual value.
400 final Method getter = YANG_TYPE_GETTERS.get(converted.getClass());
401 if (getter != null) {
402 converted = getter.invoke(converted);
408 private Object constructorValueRecursively(final Class<?> toType, final Object fromValue)
409 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
410 LOG.trace("convertValueRecursively - toType: {}, fromValue {} ({})",
411 toType.getSimpleName(), fromValue, fromValue.getClass().getSimpleName());
413 if (toType.equals(fromValue.getClass())) {
417 final Constructor<?> ctor = CONSTRUCTORS.get(toType);
419 if (fromValue instanceof String) {
420 final Function<String, Object> factory = UINT_FACTORIES.get(toType);
421 if (factory != null) {
422 return factory.apply((String) fromValue);
426 throw new IllegalArgumentException(String.format("Constructor not found for type %s", toType));
429 LOG.trace("Found {}", ctor);
430 Object value = fromValue;
432 // Once we find a constructor that takes the original type as an argument, we're done recursing.
433 if (!ctor.getParameterTypes()[0].equals(fromValue.getClass())) {
434 value = constructorValueRecursively(ctor.getParameterTypes()[0], fromValue);
437 return ctor.newInstance(value);