aboutsummaryrefslogtreecommitdiffstats
path: root/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java
blob: 782511f5132486ef448546b8dacc9250f44dfe8b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
/*
 * ============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:
 * 
 * <pre>
 * <code>&lt;a.prefix>.&lt;class.name> = ${event.reqid}</code>
 * </pre>
 * 
 * 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<String, Extractor> 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.info("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.
     * <p>
     * Note: this will <i>not</i> 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<Extractor, Class<?>> 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<Extractor, Class<?>> buildExtractor(Class<?> clazz, String comp) throws ExtractorException {
            Pair<Extractor, Class<?>> 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<Extractor, Class<?>> 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<Extractor, Class<?>> 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<Extractor, Class<?>> 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;
        }
    }
}