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 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.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.util.AbstractMap.SimpleImmutableEntry;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
30 import java.util.Map.Entry;
32 import javax.annotation.concurrent.GuardedBy;
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.commons.text.WordUtils;
35 import org.opendaylight.controller.cluster.datastore.DatastoreContext.Builder;
36 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
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.binding.InstanceIdentifier;
40 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Introspects on a DatastoreContext instance to set its properties via reflection.
49 * @author Thomas Pantelis
51 public class DatastoreContextIntrospector {
52 private static final Logger LOG = LoggerFactory.getLogger(DatastoreContextIntrospector.class);
54 private static final Map<String, Entry<Class<?>, Method>> DATA_STORE_PROP_INFO = new HashMap<>();
56 private static final Map<Class<?>, Constructor<?>> CONSTRUCTORS = new HashMap<>();
58 private static final Map<Class<?>, Method> YANG_TYPE_GETTERS = new HashMap<>();
60 private static final Map<String, Method> BUILDER_SETTERS = new HashMap<>();
64 introspectDatastoreContextBuilder();
65 introspectDataStoreProperties();
66 introspectPrimitiveTypes();
67 } catch (final IntrospectionException e) {
68 LOG.error("Error initializing DatastoreContextIntrospector", e);
73 * Introspects each primitive wrapper (ie Integer, Long etc) and String type to find the
74 * constructor that takes a single String argument. For primitive wrappers, this constructor
75 * converts from a String representation.
77 // Disables "Either log or rethrow this exception" sonar warning
78 @SuppressWarnings("squid:S1166")
79 private static void introspectPrimitiveTypes() {
80 final Set<Class<?>> primitives = ImmutableSet.<Class<?>>builder().addAll(
81 Primitives.allWrapperTypes()).add(String.class).build();
82 for (final Class<?> primitive: primitives) {
84 processPropertyType(primitive);
85 } catch (final NoSuchMethodException e) {
86 // Ignore primitives that can't be constructed from a String, eg Character and Void.
87 } catch (SecurityException | IntrospectionException e) {
88 LOG.error("Error introspect primitive type {}", primitive, e);
94 * Introspects the DatastoreContext.Builder class to find all its setter methods that we will
95 * invoke via reflection. We can't use the bean Introspector here as the Builder setters don't
96 * follow the bean property naming convention, ie setter prefixed with "set", so look for all
97 * the methods that return Builder.
99 private static void introspectDatastoreContextBuilder() {
100 for (final Method method: Builder.class.getMethods()) {
101 if (Builder.class.equals(method.getReturnType())) {
102 BUILDER_SETTERS.put(method.getName(), method);
108 * Introspects the DataStoreProperties interface that is generated from the DataStoreProperties
109 * yang grouping. We use the bean Introspector to find the types of all the properties defined
110 * in the interface (this is the type returned from the getter method). For each type, we find
111 * the appropriate constructor that we will use.
113 private static void introspectDataStoreProperties() throws IntrospectionException {
114 final BeanInfo beanInfo = Introspector.getBeanInfo(DataStoreProperties.class);
115 for (final PropertyDescriptor desc: beanInfo.getPropertyDescriptors()) {
116 processDataStoreProperty(desc.getName(), desc.getPropertyType(), desc.getReadMethod());
119 // Getter methods that return Boolean and start with "is" instead of "get" aren't recognized as
120 // properties and thus aren't returned from getPropertyDescriptors. A getter starting with
121 // "is" is only supported if it returns primitive boolean. So we'll check for these via
122 // getMethodDescriptors.
123 for (final MethodDescriptor desc: beanInfo.getMethodDescriptors()) {
124 final String methodName = desc.getName();
125 if (Boolean.class.equals(desc.getMethod().getReturnType()) && methodName.startsWith("is")) {
126 final String propertyName = WordUtils.uncapitalize(methodName.substring(2));
127 processDataStoreProperty(propertyName, Boolean.class, desc.getMethod());
133 * Processes a property defined on the DataStoreProperties interface.
135 @SuppressWarnings("checkstyle:IllegalCatch")
136 private static void processDataStoreProperty(final String name, final Class<?> propertyType, Method readMethod) {
137 Preconditions.checkArgument(BUILDER_SETTERS.containsKey(name), String.format(
138 "DataStoreProperties property \"%s\" does not have corresponding setter in DatastoreContext.Builder",
141 processPropertyType(propertyType);
142 DATA_STORE_PROP_INFO.put(name, new SimpleImmutableEntry<>(propertyType, readMethod));
143 } catch (final Exception e) {
144 LOG.error("Error finding constructor for type {}", propertyType, e);
149 * Finds the appropriate constructor for the specified type that we will use to construct
152 private static void processPropertyType(final Class<?> propertyType)
153 throws NoSuchMethodException, SecurityException, IntrospectionException {
154 final Class<?> wrappedType = Primitives.wrap(propertyType);
155 if (CONSTRUCTORS.containsKey(wrappedType)) {
159 // If the type is a primitive (or String type), we look for the constructor that takes a
160 // single String argument, which, for primitives, validates and converts from a String
161 // representation which is the form we get on ingress.
162 if (propertyType.isPrimitive() || Primitives.isWrapperType(propertyType) || propertyType.equals(String.class)) {
163 CONSTRUCTORS.put(wrappedType, propertyType.getConstructor(String.class));
165 // This must be a yang-defined type. We need to find the constructor that takes a
166 // primitive as the only argument. This will be used to construct instances to perform
167 // validation (eg range checking). The yang-generated types have a couple single-argument
168 // constructors but the one we want has the bean ConstructorProperties annotation.
169 for (final Constructor<?> ctor: propertyType.getConstructors()) {
170 final ConstructorProperties ctorPropsAnnotation = ctor.getAnnotation(ConstructorProperties.class);
171 if (ctor.getParameterTypes().length == 1 && ctorPropsAnnotation != null) {
172 findYangTypeGetter(propertyType, ctorPropsAnnotation.value()[0]);
173 CONSTRUCTORS.put(propertyType, ctor);
181 * Finds the getter method on a yang-generated type for the specified property name.
183 private static void findYangTypeGetter(final Class<?> type, final String propertyName)
184 throws IntrospectionException {
185 for (final PropertyDescriptor desc: Introspector.getBeanInfo(type).getPropertyDescriptors()) {
186 if (desc.getName().equals(propertyName)) {
187 YANG_TYPE_GETTERS.put(type, desc.getReadMethod());
192 throw new IntrospectionException(String.format(
193 "Getter method for constructor property %s not found for YANG type %s",
194 propertyName, type));
197 @GuardedBy(value = "this")
198 private DatastoreContext context;
199 @GuardedBy(value = "this")
200 private Map<String, Object> currentProperties;
202 public DatastoreContextIntrospector(final DatastoreContext context,
203 final BindingNormalizedNodeSerializer bindingSerializer) {
204 final QName qname = BindingReflections.findQName(DataStorePropertiesContainer.class);
205 final DataStorePropertiesContainer defaultPropsContainer = (DataStorePropertiesContainer)
206 bindingSerializer.fromNormalizedNode(bindingSerializer.toYangInstanceIdentifier(
207 InstanceIdentifier.builder(DataStorePropertiesContainer.class).build()),
208 ImmutableNodes.containerNode(qname)).getValue();
210 final Builder builder = DatastoreContext.newBuilderFrom(context);
211 for (Entry<String, Entry<Class<?>, Method>> entry: DATA_STORE_PROP_INFO.entrySet()) {
214 value = entry.getValue().getValue().invoke(defaultPropsContainer);
215 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
216 LOG.error("Error obtaining default value for property {}", entry.getKey(), e);
221 convertValueAndInvokeSetter(entry.getKey(), value, builder);
225 this.context = builder.build();
228 public synchronized DatastoreContext getContext() {
232 public DatastoreContextFactory newContextFactory() {
233 return new DatastoreContextFactory(this);
236 public synchronized DatastoreContext getShardDatastoreContext(final String forShardName) {
237 if (currentProperties == null) {
241 final Builder builder = DatastoreContext.newBuilderFrom(context);
242 final String dataStoreTypePrefix = context.getDataStoreName() + '.';
243 final String shardNamePrefix = forShardName + '.';
245 final List<String> keys = getSortedKeysByDatastoreType(currentProperties.keySet(), dataStoreTypePrefix);
247 for (String key: keys) {
248 final Object value = currentProperties.get(key);
249 if (key.startsWith(dataStoreTypePrefix)) {
250 key = key.replaceFirst(dataStoreTypePrefix, "");
253 if (key.startsWith(shardNamePrefix)) {
254 key = key.replaceFirst(shardNamePrefix, "");
255 convertValueAndInvokeSetter(key, value.toString(), builder);
259 return builder.build();
263 * Applies the given properties to the cached DatastoreContext and yields a new DatastoreContext
264 * instance which can be obtained via {@link #getContext()}.
266 * @param properties the properties to apply
267 * @return true if the cached DatastoreContext was updated, false otherwise.
269 public synchronized boolean update(final Map<String, Object> properties) {
270 currentProperties = null;
271 if (properties == null || properties.isEmpty()) {
275 LOG.debug("In update: properties: {}", properties);
277 final ImmutableMap.Builder<String, Object> mapBuilder = ImmutableMap.<String, Object>builder();
279 final Builder builder = DatastoreContext.newBuilderFrom(context);
281 final String dataStoreTypePrefix = context.getDataStoreName() + '.';
283 final List<String> keys = getSortedKeysByDatastoreType(properties.keySet(), dataStoreTypePrefix);
285 boolean updated = false;
286 for (String key: keys) {
287 final Object value = properties.get(key);
288 mapBuilder.put(key, value);
290 // If the key is prefixed with the data store type, strip it off.
291 if (key.startsWith(dataStoreTypePrefix)) {
292 key = key.replaceFirst(dataStoreTypePrefix, "");
295 if (convertValueAndInvokeSetter(key, value.toString(), builder)) {
300 currentProperties = mapBuilder.build();
303 context = builder.build();
309 private static ArrayList<String> getSortedKeysByDatastoreType(final Collection<String> inKeys,
310 final String dataStoreTypePrefix) {
311 // Sort the property keys by putting the names prefixed with the data store type last. This
312 // is done so data store specific settings are applied after global settings.
313 final ArrayList<String> keys = new ArrayList<>(inKeys);
314 Collections.sort(keys, (key1, key2) -> key1.startsWith(dataStoreTypePrefix) ? 1 :
315 key2.startsWith(dataStoreTypePrefix) ? -1 : key1.compareTo(key2));
319 @SuppressWarnings("checkstyle:IllegalCatch")
320 private boolean convertValueAndInvokeSetter(final String inKey, final Object inValue, final Builder builder) {
321 final String key = convertToCamelCase(inKey);
324 // Convert the value to the right type.
325 final Object value = convertValue(key, inValue);
330 LOG.debug("Converted value for property {}: {} ({})",
331 key, value, value.getClass().getSimpleName());
333 // Call the setter method on the Builder instance.
334 final Method setter = BUILDER_SETTERS.get(key);
335 setter.invoke(builder, constructorValueRecursively(
336 Primitives.wrap(setter.getParameterTypes()[0]), value.toString()));
339 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
340 | InstantiationException e) {
341 LOG.error("Error converting value ({}) for property {}", inValue, key, e);
347 private static String convertToCamelCase(final String inString) {
348 String str = inString.trim();
349 if (StringUtils.contains(str, '-') || StringUtils.contains(str, ' ')) {
350 str = inString.replace('-', ' ');
351 str = WordUtils.capitalizeFully(str);
352 str = StringUtils.deleteWhitespace(str);
355 return StringUtils.uncapitalize(str);
358 private Object convertValue(final String name, final Object from)
359 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
360 final Entry<Class<?>, Method> propertyInfo = DATA_STORE_PROP_INFO.get(name);
361 if (propertyInfo == null) {
362 LOG.debug("Property not found for {}", name);
366 final Class<?> propertyType = propertyInfo.getKey();
368 LOG.debug("Type for property {}: {}, converting value {} ({})",
369 name, propertyType.getSimpleName(), from, from.getClass().getSimpleName());
371 // Recurse the chain of constructors depth-first to get the resulting value. Eg, if the
372 // property type is the yang-generated NonZeroUint32Type, it's constructor takes a Long so
373 // we have to first construct a Long instance from the input value.
374 Object converted = constructorValueRecursively(propertyType, from);
376 // If the converted type is a yang-generated type, call the getter to obtain the actual value.
377 final Method getter = YANG_TYPE_GETTERS.get(converted.getClass());
378 if (getter != null) {
379 converted = getter.invoke(converted);
385 private Object constructorValueRecursively(final Class<?> toType, final Object fromValue)
386 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
387 LOG.trace("convertValueRecursively - toType: {}, fromValue {} ({})",
388 toType.getSimpleName(), fromValue, fromValue.getClass().getSimpleName());
390 if (toType.equals(fromValue.getClass())) {
394 final Constructor<?> ctor = CONSTRUCTORS.get(toType);
396 LOG.trace("Found {}", ctor);
399 throw new IllegalArgumentException(String.format("Constructor not found for type %s", toType));
402 Object value = fromValue;
404 // Once we find a constructor that takes the original type as an argument, we're done recursing.
405 if (!ctor.getParameterTypes()[0].equals(fromValue.getClass())) {
406 value = constructorValueRecursively(ctor.getParameterTypes()[0], fromValue);
409 return ctor.newInstance(value);