Re-encrypt drools-pdp properties
[policy/drools-pdp.git] / feature-pooling-dmaap / src / main / java / org / onap / policy / drools / pooling / extractor / ClassExtractors.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP
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
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
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=========================================================
20  */
21
22 package org.onap.policy.drools.pooling.extractor;
23
24 import java.lang.reflect.Field;
25 import java.lang.reflect.Method;
26 import java.util.Map;
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;
33
34 /**
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
37  * following:
38  *
39  * <pre>
40  * <code>&lt;a.prefix>.&lt;class.name> = ${event.reqid}</code>
41  * </pre>
42  *
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.
47  */
48 public class ClassExtractors {
49
50     private static final Logger logger = LoggerFactory.getLogger(ClassExtractors.class);
51
52     /**
53      * Properties that specify how the data is to be extracted from a given
54      * class.
55      */
56     private final Properties properties;
57
58     /**
59      * Property prefix, including a trailing ".".
60      */
61     private final String prefix;
62
63     /**
64      * Type of item to be extracted.
65      */
66     private final String type;
67
68     /**
69      * Maps the class name to its extractor.
70      */
71     private final ConcurrentHashMap<String, Extractor> class2extractor = new ConcurrentHashMap<>();
72
73     /**
74      * Constructor.
75      *
76      * @param props properties that specify how the data is to be extracted from
77      *        a given class
78      * @param prefix property name prefix, prepended before the class name
79      * @param type type of item to be extracted
80      */
81     public ClassExtractors(Properties props, String prefix, String type) {
82         this.properties = props;
83         this.prefix = (prefix.endsWith(".") ? prefix : prefix + ".");
84         this.type = type;
85     }
86
87     /**
88      * Gets the number of extractors in the map.
89      *
90      * @return gets the number of extractors in the map
91      */
92     protected int size() {
93         return class2extractor.size();
94     }
95
96     /**
97      * Extracts the desired data item from an object.
98      *
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
101      */
102     public Object extract(Object object) {
103         if (object == null) {
104             return null;
105         }
106
107         Extractor ext = getExtractor(object);
108
109         return ext.extract(object);
110     }
111
112     /**
113      * Gets the extractor for the given type of object, creating one if it
114      * doesn't exist yet.
115      *
116      * @param object object whose extracted is desired
117      * @return an extractor for the object
118      */
119     private Extractor getExtractor(Object object) {
120         Class<?> clazz = object.getClass();
121         Extractor ext = class2extractor.get(clazz.getName());
122
123         if (ext == null) {
124             // allocate a new extractor, if another thread doesn't beat us to it
125             ext = class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
126         }
127
128         return ext;
129     }
130
131     /**
132      * Builds an extractor for the class.
133      *
134      * @param clazz class for which the extractor should be built
135      *
136      * @return a new extractor
137      */
138     private Extractor buildExtractor(Class<?> clazz) {
139         String value = properties.getProperty(prefix + clazz.getName(), null);
140         if (value != null) {
141             // property has config info for this class - build the extractor
142             return buildExtractor(clazz, value);
143         }
144
145         /*
146          * Get the extractor, if any, for the super class or interfaces, but
147          * don't add one if it doesn't exist
148          */
149         Extractor ext = getClassExtractor(clazz, false);
150         if (ext != null) {
151             return ext;
152         }
153
154         /*
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.
158          */
159         logger.warn("missing property {}{}", prefix, clazz.getName());
160         return new NullExtractor();
161     }
162
163     /**
164      * Builds an extractor for the class, based on the config value extracted
165      * from the corresponding property.
166      *
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
170      */
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();
175         }
176
177         if (!value.endsWith("}")) {
178             logger.warn("property value for {}{} does not end with '}'", prefix, clazz.getName());
179             return new NullExtractor();
180         }
181
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();
187         }
188
189         if (val.endsWith(".")) {
190             logger.warn("property value for {}{} ends with '.'", prefix, clazz.getName());
191             return new NullExtractor();
192         }
193
194         // everything's valid - create the extractor
195         try {
196             ComponetizedExtractor ext = new ComponetizedExtractor(clazz, val.split("[.]"));
197
198             /*
199              * If there's only one extractor, then just return it, otherwise
200              * return the whole extractor.
201              */
202             return (ext.extractors.length == 1 ? ext.extractors[0] : ext);
203
204         } catch (ExtractorException e) {
205             logger.warn("cannot build extractor for {}", clazz.getName(), e);
206             return new NullExtractor();
207         }
208     }
209
210     /**
211      * Gets the extractor for a class, examining all super classes and
212      * interfaces.
213      *
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
219      */
220     private Extractor getClassExtractor(Class<?> clazz, boolean addOk) {
221         if (clazz == null) {
222             return null;
223         }
224
225         Extractor ext = null;
226
227         if (addOk) {
228             String val = properties.getProperty(prefix + clazz.getName(), null);
229
230             if (val != null) {
231                 /*
232                  * A property is defined for this class, so create the extractor
233                  * for it.
234                  */
235                 return class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
236             }
237         }
238
239         // see if the superclass has an extractor
240         if ((ext = getClassExtractor(clazz.getSuperclass(), true)) != null) {
241             return ext;
242         }
243
244         // check the interfaces, too
245         for (Class<?> clz : clazz.getInterfaces()) {
246             if ((ext = getClassExtractor(clz, true)) != null) {
247                 break;
248             }
249         }
250
251         return ext;
252     }
253
254     /**
255      * Extractor that always returns {@code null}. Used when no extractor could
256      * be built for a given object type.
257      */
258     private class NullExtractor implements Extractor {
259
260         @Override
261         public Object extract(Object object) {
262             logger.info("cannot extract {} from {}", type, object.getClass());
263             return null;
264         }
265     }
266
267     /**
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.
272      *
273      * <p>Note: this will <i>not</i> work if POJOs are contained within a Map.
274      */
275     private class ComponetizedExtractor implements Extractor {
276
277         /**
278          * Extractor for each component.
279          */
280         private final Extractor[] extractors;
281
282         /**
283          * Constructor.
284          *
285          * @param clazz the class associated with the object at the root of the
286          *        hierarchy
287          * @param names name associated with each component
288          * @throws ExtractorException extractor exception
289          */
290         public ComponetizedExtractor(Class<?> clazz, String[] names) throws ExtractorException {
291             this.extractors = new Extractor[names.length];
292
293             Class<?> clz = clazz;
294
295             for (int x = 0; x < names.length; ++x) {
296                 String comp = names[x];
297
298                 Pair<Extractor, Class<?>> pair = buildExtractor(clz, comp);
299
300                 extractors[x] = pair.getLeft();
301                 clz = pair.getRight();
302             }
303         }
304
305         /**
306          * Builds an extractor for the given component of an object.
307          *
308          * @param clazz type of object from which the component will be
309          *        extracted
310          * @param comp name of the component to extract
311          * @return a pair containing the extractor and the extracted object's
312          *         type
313          * @throws ExtractorException extrator exception
314          */
315         private Pair<Extractor, Class<?>> buildExtractor(Class<?> clazz, String comp) throws ExtractorException {
316
317             Pair<Extractor, Class<?>> pair = getMethodExtractor(clazz, comp);
318
319             if (pair == null) {
320                 pair = getFieldExtractor(clazz, comp);
321             }
322
323             if (pair == null) {
324                 pair = getMapExtractor(clazz, comp);
325             }
326
327
328             // didn't find an extractor
329             if (pair == null) {
330                 throw new ExtractorException("class " + clazz + " contains no element " + comp);
331             }
332
333             return pair;
334         }
335
336         @Override
337         public Object extract(Object object) {
338             Object obj = object;
339
340             for (Extractor ext : extractors) {
341                 if (obj == null) {
342                     break;
343                 }
344
345                 obj = ext.extract(obj);
346             }
347
348             return obj;
349         }
350
351         /**
352          * Gets an extractor that invokes a getXxx() method to retrieve the
353          * object.
354          *
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
360          */
361         private Pair<Extractor, Class<?>> getMethodExtractor(Class<?> clazz, String name) throws ExtractorException {
362             Method meth;
363
364             String nm = "get" + StringUtils.capitalize(name);
365
366             try {
367                 meth = clazz.getMethod(nm);
368
369                 Class<?> retType = meth.getReturnType();
370                 if (retType == void.class) {
371                     // it's a void method, thus it won't return an object
372                     return null;
373                 }
374
375                 return Pair.of(new MethodExtractor(meth), retType);
376
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);
380                 return null;
381
382             } catch (SecurityException e) {
383                 throw new ExtractorException("inaccessible method " + clazz + "." + nm, e);
384             }
385         }
386
387         /**
388          * Gets an extractor for a field within the object.
389          *
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
395          */
396         private Pair<Extractor, Class<?>> getFieldExtractor(Class<?> clazz, String name) throws ExtractorException {
397
398             Field field = getClassField(clazz, name);
399             if (field == null) {
400                 return null;
401             }
402
403             return Pair.of(new FieldExtractor(field), field.getType());
404         }
405
406         /**
407          * Gets an extractor for an item within a Map object.
408          *
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
412          *         subclass
413          */
414         private Pair<Extractor, Class<?>> getMapExtractor(Class<?> clazz, String key) {
415
416             if (!Map.class.isAssignableFrom(clazz)) {
417                 return null;
418             }
419
420             /*
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.
426              */
427             return Pair.of(new MapExtractor(key), Map.class);
428         }
429
430         /**
431          * Gets field within a class, examining all super classes and
432          * interfaces.
433          *
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
437          *         not exist
438          * @throws ExtractorException if the field is inaccessible
439          */
440         private Field getClassField(Class<?> clazz, String name) throws ExtractorException {
441             if (clazz == null) {
442                 return null;
443             }
444
445             try {
446                 return clazz.getField(name);
447
448             } catch (NoSuchFieldException expected) {
449                 // no field by this name - try super class & interfaces
450                 logger.debug("no field {} in {}", name, clazz.getName(), expected);
451                 return null;
452
453             } catch (SecurityException e) {
454                 throw new ExtractorException("inaccessible field " + clazz + "." + name, e);
455             }
456         }
457     }
458 }