/* * ============LICENSE_START======================================================= * ONAP * ================================================================================ * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============LICENSE_END========================================================= */ package org.onap.policy.drools.pooling.extractor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang.StringUtils; import org.onap.policy.drools.utils.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Extractors for each object class. Properties define how the data is to be * extracted for a given class, where the properties are similar to the * following: * *
 * <a.prefix>.<class.name> = ${event.reqid}
 * 
* * If it doesn't find a property for the class, then it looks for a property for * that class' super class or interfaces. Extractors are compiled and cached. */ public class ClassExtractors { private static final Logger logger = LoggerFactory.getLogger(ClassExtractors.class); /** * Properties that specify how the data is to be extracted from a given * class. */ private final Properties properties; /** * Property prefix, including a trailing ".". */ private final String prefix; /** * Type of item to be extracted. */ private final String type; /** * Maps the class name to its extractor. */ private final ConcurrentHashMap class2extractor = new ConcurrentHashMap<>(); /** * * @param props properties that specify how the data is to be extracted from * a given class * @param prefix property name prefix, prepended before the class name * @param type type of item to be extracted */ public ClassExtractors(Properties props, String prefix, String type) { this.properties = props; this.prefix = (prefix.endsWith(".") ? prefix : prefix + "."); this.type = type; } /** * Gets the number of extractors in the map. * * @return gets the number of extractors in the map */ protected int size() { return class2extractor.size(); } /** * Extracts the desired data item from an object. * * @param object object from which to extract the data item * @return the extracted item, or {@code null} if it could not be extracted */ public Object extract(Object object) { if (object == null) { return null; } Extractor ext = getExtractor(object); return ext.extract(object); } /** * Gets the extractor for the given type of object, creating one if it * doesn't exist yet. * * @param object object whose extracted is desired * @return an extractor for the object */ private Extractor getExtractor(Object object) { Class clazz = object.getClass(); Extractor ext = class2extractor.get(clazz.getName()); if (ext == null) { // allocate a new extractor, if another thread doesn't beat us to it ext = class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz)); } return ext; } /** * Builds an extractor for the class. * * @param clazz class for which the extractor should be built * * @return a new extractor */ private Extractor buildExtractor(Class clazz) { String value = properties.getProperty(prefix + clazz.getName(), null); if (value != null) { // property has config info for this class - build the extractor return buildExtractor(clazz, value); } /* * Get the extractor, if any, for the super class or interfaces, but * don't add one if it doesn't exist */ Extractor ext = getClassExtractor(clazz, false); if (ext != null) { return ext; } /* * No extractor defined for for this class or its super class - we * cannot extract data items from objects of this type, so just * allocated a null extractor. */ logger.warn("missing property {}{}", prefix, clazz.getName()); return new NullExtractor(); } /** * Builds an extractor for the class, based on the config value extracted * from the corresponding property. * * @param clazz class for which the extractor should be built * @param value config value (e.g., "${event.request.id}" * @return a new extractor */ private Extractor buildExtractor(Class clazz, String value) { if (!value.startsWith("${")) { logger.warn("property value for {}{} does not start with '${'", prefix, clazz.getName()); return new NullExtractor(); } if (!value.endsWith("}")) { logger.warn("property value for {}{} does not end with '}'", prefix, clazz.getName()); return new NullExtractor(); } // get the part in the middle String val = value.substring(2, value.length() - 1); if (val.startsWith(".")) { logger.warn("property value for {}{} begins with '.'", prefix, clazz.getName()); return new NullExtractor(); } if (val.endsWith(".")) { logger.warn("property value for {}{} ends with '.'", prefix, clazz.getName()); return new NullExtractor(); } // everything's valid - create the extractor try { ComponetizedExtractor ext = new ComponetizedExtractor(clazz, val.split("[.]")); /* * If there's only one extractor, then just return it, otherwise * return the whole extractor. */ return (ext.extractors.length == 1 ? ext.extractors[0] : ext); } catch (ExtractorException e) { logger.warn("cannot build extractor for {}", clazz.getName(), e); return new NullExtractor(); } } /** * Gets the extractor for a class, examining all super classes and * interfaces. * * @param clazz class whose extractor is desired * @param addOk {@code true} if the extractor may be added, provided the * property is defined, {@code false} otherwise * @return the extractor to be used for the class, or {@code null} if no * extractor has been defined yet */ private Extractor getClassExtractor(Class clazz, boolean addOk) { if (clazz == null) { return null; } Extractor ext = null; if (addOk) { String val = properties.getProperty(prefix + clazz.getName(), null); if (val != null) { /* * A property is defined for this class, so create the extractor * for it. */ return class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz)); } } // see if the superclass has an extractor if ((ext = getClassExtractor(clazz.getSuperclass(), true)) != null) { return ext; } // check the interfaces, too for (Class clz : clazz.getInterfaces()) { if ((ext = getClassExtractor(clz, true)) != null) { break; } } return ext; } /** * Extractor that always returns {@code null}. Used when no extractor could * be built for a given object type. */ private class NullExtractor implements Extractor { @Override public Object extract(Object object) { logger.warn("cannot extract {} from {}", type, object.getClass()); return null; } } /** * Component-ized extractor. Extracts an object that is referenced * hierarchically, where each name identifies a particular component within * the hierarchy. Supports retrieval from {@link Map} objects, as well as * via getXxx() methods, or by direct field retrieval. *

* Note: this will not work if POJOs are contained within a Map. */ private class ComponetizedExtractor implements Extractor { /** * Extractor for each component. */ private final Extractor[] extractors; /** * * @param clazz the class associated with the object at the root of the * hierarchy * @param names name associated with each component * @throws ExtractorException */ public ComponetizedExtractor(Class clazz, String[] names) throws ExtractorException { this.extractors = new Extractor[names.length]; Class clz = clazz; for (int x = 0; x < names.length; ++x) { String comp = names[x]; Pair> pair = buildExtractor(clz, comp); extractors[x] = pair.first(); clz = pair.second(); } } /** * Builds an extractor for the given component of an object. * * @param clazz type of object from which the component will be * extracted * @param comp name of the component to extract * @return a pair containing the extractor and the extracted object's * type * @throws ExtractorException */ private Pair> buildExtractor(Class clazz, String comp) throws ExtractorException { Pair> pair = null; if (pair == null) { pair = getMethodExtractor(clazz, comp); } if (pair == null) { pair = getFieldExtractor(clazz, comp); } if (pair == null) { pair = getMapExtractor(clazz, comp); } // didn't find an extractor if (pair == null) { throw new ExtractorException("class " + clazz + " contains no element " + comp); } return pair; } @Override public Object extract(Object object) { Object obj = object; for (Extractor ext : extractors) { if (obj == null) { break; } obj = ext.extract(obj); } return obj; } /** * Gets an extractor that invokes a getXxx() method to retrieve the * object. * * @param clazz container's class * @param name name of the property to be retrieved * @return a new extractor, or {@code null} if the class does not * contain the corresponding getXxx() method * @throws ExtractorException if the getXxx() method is inaccessible */ private Pair> getMethodExtractor(Class clazz, String name) throws ExtractorException { Method meth; String nm = "get" + StringUtils.capitalize(name); try { meth = clazz.getMethod(nm); Class retType = meth.getReturnType(); if (retType == void.class) { // it's a void method, thus it won't return an object return null; } return new Pair<>(new MethodExtractor(meth), retType); } catch (NoSuchMethodException expected) { // no getXxx() method, maybe there's a field by this name logger.debug("no method {} in {}", nm, clazz.getName(), expected); return null; } catch (SecurityException e) { throw new ExtractorException("inaccessible method " + clazz + "." + nm, e); } } /** * Gets an extractor for a field within the object. * * @param clazz container's class * @param name name of the field whose value is to be extracted * @return a new extractor, or {@code null} if the class does not * contain the given field * @throws ExtractorException if the field is inaccessible */ private Pair> getFieldExtractor(Class clazz, String name) throws ExtractorException { Field field = getClassField(clazz, name); if (field == null) { return null; } return new Pair<>(new FieldExtractor(field), field.getType()); } /** * Gets an extractor for an item within a Map object. * * @param clazz container's class * @param key item key within the map * @return a new extractor, or {@code null} if the class is not a Map * subclass */ private Pair> getMapExtractor(Class clazz, String key) { if (!Map.class.isAssignableFrom(clazz)) { return null; } /* * Don't know the value's actual type, so we'll assume it's a Map * for now. Things should still work OK, as this is only used to * direct the constructor on what type of extractor to create next. * If the object turns out not to be a map, then the MapExtractor * for the next component will just return null. */ return new Pair<>(new MapExtractor(key), Map.class); } /** * Gets field within a class, examining all super classes and * interfaces. * * @param clazz class whose field is desired * @param name name of the desired field * @return the field within the class, or {@code null} if the field does * not exist * @throws ExtractorException if the field is inaccessible */ private Field getClassField(Class clazz, String name) throws ExtractorException { if (clazz == null) { return null; } try { return clazz.getDeclaredField(name); } catch (NoSuchFieldException expected) { // no field by this name - try super class & interfaces logger.debug("no field {} in {}", name, clazz.getName(), expected); } catch (SecurityException e) { throw new ExtractorException("inaccessible field " + clazz + "." + name, e); } Field field; // see if the superclass has an extractor if ((field = getClassField(clazz.getSuperclass(), name)) != null) { return field; } // not necessary to check the interfaces return field; } } }