2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2019-2020 Nordix Foundation
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.drools.pooling.extractor;
24 import java.lang.reflect.Field;
25 import java.lang.reflect.Method;
27 import java.util.Properties;
28 import java.util.concurrent.ConcurrentHashMap;
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.commons.lang3.tuple.Pair;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * Extractors for each object class. Properties define how the data is to be
36 * extracted for a given class, where the properties are similar to the
40 * <code><a.prefix>.<class.name> = ${event.reqid}</code>
43 * <p>For any given field name (e.g., "reqid"), it first looks for a public "getXxx()"
44 * method to extract the specified field. If that fails, then it looks for a public field
45 * by the given name. If that also fails, and the object is a <i>Map</i> subclass, then it
46 * simply uses the "get(field-name)" method to extract the data from the map.
48 public class ClassExtractors {
50 private static final Logger logger = LoggerFactory.getLogger(ClassExtractors.class);
53 * Properties that specify how the data is to be extracted from a given
56 private final Properties properties;
59 * Property prefix, including a trailing ".".
61 private final String prefix;
64 * Type of item to be extracted.
66 private final String type;
69 * Maps the class name to its extractor.
71 private final ConcurrentHashMap<String, Extractor> class2extractor = new ConcurrentHashMap<>();
76 * @param props properties that specify how the data is to be extracted from
78 * @param prefix property name prefix, prepended before the class name
79 * @param type type of item to be extracted
81 public ClassExtractors(Properties props, String prefix, String type) {
82 this.properties = props;
83 this.prefix = (prefix.endsWith(".") ? prefix : prefix + ".");
88 * Gets the number of extractors in the map.
90 * @return gets the number of extractors in the map
92 protected int size() {
93 return class2extractor.size();
97 * Extracts the desired data item from an object.
99 * @param object object from which to extract the data item
100 * @return the extracted item, or {@code null} if it could not be extracted
102 public Object extract(Object object) {
103 if (object == null) {
107 Extractor ext = getExtractor(object);
109 return ext.extract(object);
113 * Gets the extractor for the given type of object, creating one if it
116 * @param object object whose extracted is desired
117 * @return an extractor for the object
119 private Extractor getExtractor(Object object) {
120 Class<?> clazz = object.getClass();
121 Extractor ext = class2extractor.get(clazz.getName());
124 // allocate a new extractor, if another thread doesn't beat us to it
125 ext = class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
132 * Builds an extractor for the class.
134 * @param clazz class for which the extractor should be built
136 * @return a new extractor
138 private Extractor buildExtractor(Class<?> clazz) {
139 String value = properties.getProperty(prefix + clazz.getName(), null);
141 // property has config info for this class - build the extractor
142 return buildExtractor(clazz, value);
146 * Get the extractor, if any, for the super class or interfaces, but
147 * don't add one if it doesn't exist
149 Extractor ext = getClassExtractor(clazz, false);
155 * No extractor defined for for this class or its super class - we
156 * cannot extract data items from objects of this type, so just
157 * allocated a null extractor.
159 logger.warn("missing property {}{}", prefix, clazz.getName());
160 return new NullExtractor();
164 * Builds an extractor for the class, based on the config value extracted
165 * from the corresponding property.
167 * @param clazz class for which the extractor should be built
168 * @param value config value (e.g., "${event.request.id}"
169 * @return a new extractor
171 private Extractor buildExtractor(Class<?> clazz, String value) {
172 if (!value.startsWith("${")) {
173 logger.warn("property value for {}{} does not start with {}", prefix, clazz.getName(), "'${'");
174 return new NullExtractor();
177 if (!value.endsWith("}")) {
178 logger.warn("property value for {}{} does not end with '}'", prefix, clazz.getName());
179 return new NullExtractor();
182 // get the part in the middle
183 String val = value.substring(2, value.length() - 1);
184 if (val.startsWith(".")) {
185 logger.warn("property value for {}{} begins with '.'", prefix, clazz.getName());
186 return new NullExtractor();
189 if (val.endsWith(".")) {
190 logger.warn("property value for {}{} ends with '.'", prefix, clazz.getName());
191 return new NullExtractor();
194 // everything's valid - create the extractor
196 ComponetizedExtractor ext = new ComponetizedExtractor(clazz, val.split("[.]"));
199 * If there's only one extractor, then just return it, otherwise
200 * return the whole extractor.
202 return (ext.extractors.length == 1 ? ext.extractors[0] : ext);
204 } catch (ExtractorException e) {
205 logger.warn("cannot build extractor for {}", clazz.getName(), e);
206 return new NullExtractor();
211 * Gets the extractor for a class, examining all super classes and
214 * @param clazz class whose extractor is desired
215 * @param addOk {@code true} if the extractor may be added, provided the
216 * property is defined, {@code false} otherwise
217 * @return the extractor to be used for the class, or {@code null} if no
218 * extractor has been defined yet
220 private Extractor getClassExtractor(Class<?> clazz, boolean addOk) {
225 Extractor ext = null;
228 String val = properties.getProperty(prefix + clazz.getName(), null);
232 * A property is defined for this class, so create the extractor
235 return class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
239 // see if the superclass has an extractor
240 if ((ext = getClassExtractor(clazz.getSuperclass(), true)) != null) {
244 // check the interfaces, too
245 for (Class<?> clz : clazz.getInterfaces()) {
246 if ((ext = getClassExtractor(clz, true)) != null) {
255 * Extractor that always returns {@code null}. Used when no extractor could
256 * be built for a given object type.
258 private class NullExtractor implements Extractor {
261 public Object extract(Object object) {
262 logger.info("cannot extract {} from {}", type, object.getClass());
268 * Component-ized extractor. Extracts an object that is referenced
269 * hierarchically, where each name identifies a particular component within
270 * the hierarchy. Supports retrieval from {@link Map} objects, as well as
271 * via getXxx() methods, or by direct field retrieval.
273 * <p>Note: this will <i>not</i> work if POJOs are contained within a Map.
275 private class ComponetizedExtractor implements Extractor {
278 * Extractor for each component.
280 private final Extractor[] extractors;
285 * @param clazz the class associated with the object at the root of the
287 * @param names name associated with each component
288 * @throws ExtractorException extractor exception
290 public ComponetizedExtractor(Class<?> clazz, String[] names) throws ExtractorException {
291 this.extractors = new Extractor[names.length];
293 Class<?> clz = clazz;
295 for (int x = 0; x < names.length; ++x) {
296 String comp = names[x];
298 Pair<Extractor, Class<?>> pair = buildExtractor(clz, comp);
300 extractors[x] = pair.getLeft();
301 clz = pair.getRight();
306 * Builds an extractor for the given component of an object.
308 * @param clazz type of object from which the component will be
310 * @param comp name of the component to extract
311 * @return a pair containing the extractor and the extracted object's
313 * @throws ExtractorException extrator exception
315 private Pair<Extractor, Class<?>> buildExtractor(Class<?> clazz, String comp) throws ExtractorException {
317 Pair<Extractor, Class<?>> pair = getMethodExtractor(clazz, comp);
320 pair = getFieldExtractor(clazz, comp);
324 pair = getMapExtractor(clazz, comp);
328 // didn't find an extractor
330 throw new ExtractorException("class " + clazz + " contains no element " + comp);
337 public Object extract(Object object) {
340 for (Extractor ext : extractors) {
345 obj = ext.extract(obj);
352 * Gets an extractor that invokes a getXxx() method to retrieve the
355 * @param clazz container's class
356 * @param name name of the property to be retrieved
357 * @return a new extractor, or {@code null} if the class does not
358 * contain the corresponding getXxx() method
359 * @throws ExtractorException if the getXxx() method is inaccessible
361 private Pair<Extractor, Class<?>> getMethodExtractor(Class<?> clazz, String name) throws ExtractorException {
364 String nm = "get" + StringUtils.capitalize(name);
367 meth = clazz.getMethod(nm);
369 Class<?> retType = meth.getReturnType();
370 if (retType == void.class) {
371 // it's a void method, thus it won't return an object
375 return Pair.of(new MethodExtractor(meth), retType);
377 } catch (NoSuchMethodException expected) {
378 // no getXxx() method, maybe there's a field by this name
379 logger.debug("no method {} in {}", nm, clazz.getName(), expected);
382 } catch (SecurityException e) {
383 throw new ExtractorException("inaccessible method " + clazz + "." + nm, e);
388 * Gets an extractor for a field within the object.
390 * @param clazz container's class
391 * @param name name of the field whose value is to be extracted
392 * @return a new extractor, or {@code null} if the class does not
393 * contain the given field
394 * @throws ExtractorException if the field is inaccessible
396 private Pair<Extractor, Class<?>> getFieldExtractor(Class<?> clazz, String name) throws ExtractorException {
398 Field field = getClassField(clazz, name);
403 return Pair.of(new FieldExtractor(field), field.getType());
407 * Gets an extractor for an item within a Map object.
409 * @param clazz container's class
410 * @param key item key within the map
411 * @return a new extractor, or {@code null} if the class is not a Map
414 private Pair<Extractor, Class<?>> getMapExtractor(Class<?> clazz, String key) {
416 if (!Map.class.isAssignableFrom(clazz)) {
421 * Don't know the value's actual type, so we'll assume it's a Map
422 * for now. Things should still work OK, as this is only used to
423 * direct the constructor on what type of extractor to create next.
424 * If the object turns out not to be a map, then the MapExtractor
425 * for the next component will just return null.
427 return Pair.of(new MapExtractor(key), Map.class);
431 * Gets field within a class, examining all super classes and
434 * @param clazz class whose field is desired
435 * @param name name of the desired field
436 * @return the field within the class, or {@code null} if the field does
438 * @throws ExtractorException if the field is inaccessible
440 private Field getClassField(Class<?> clazz, String name) throws ExtractorException {
446 return clazz.getField(name);
448 } catch (NoSuchFieldException expected) {
449 // no field by this name - try super class & interfaces
450 logger.debug("no field {} in {}", name, clazz.getName(), expected);
453 } catch (SecurityException e) {
454 throw new ExtractorException("inaccessible field " + clazz + "." + name, e);