cb4ce07e0765a34124a49cd2eb3890ca2520e237
[policy/drools-pdp.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.drools.protocol.coders;
22
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.Map;
28 import org.onap.policy.drools.controller.DroolsController;
29 import org.onap.policy.drools.controller.DroolsControllerConstants;
30 import org.onap.policy.drools.protocol.coders.EventProtocolCoder.CoderFilters;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * This protocol Coder that does its best attempt to decode/encode, selecting the best class and best fitted json
36  * parsing tools.
37  */
38 abstract class GenericEventProtocolCoder {
39     private static final String INVALID_ARTIFACT_ID_MSG = "Invalid artifact id";
40     private static final String INVALID_GROUP_ID_MSG = "Invalid group id";
41     private static final String INVALID_TOPIC_MSG = "Invalid Topic";
42     private static final String UNSUPPORTED_MSG = "Unsupported";
43     private static final String UNSUPPORTED_EX_MSG = "Unsupported:";
44     private static final String MISSING_CLASS = "class must be provided";
45
46     private static Logger logger = LoggerFactory.getLogger(GenericEventProtocolCoder.class);
47
48     /**
49      * Mapping topic:controller-id -> /<protocol-decoder-toolset/> where protocol-coder-toolset contains
50      * a gson-protocol-coder-toolset.
51      */
52     protected final HashMap<String, ProtocolCoderToolset> coders =
53             new HashMap<>();
54
55     /**
56      * Mapping topic + classname -> Protocol Set.
57      */
58     protected final HashMap<String, List<ProtocolCoderToolset>>
59             reverseCoders = new HashMap<>();
60
61     GenericEventProtocolCoder() {
62         super();
63     }
64
65     /**
66      * Index a new coder.
67      *
68      * @param eventProtocolParams parameter object for event encoder
69      * @throw IllegalArgumentException if an invalid parameter is passed
70      */
71     public void add(EventProtocolParams eventProtocolParams) {
72         if (eventProtocolParams.getGroupId() == null || eventProtocolParams.getGroupId().isEmpty()) {
73             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
74         }
75
76         if (eventProtocolParams.getArtifactId() == null || eventProtocolParams.getArtifactId().isEmpty()) {
77             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
78         }
79
80         if (eventProtocolParams.getTopic() == null || eventProtocolParams.getTopic().isEmpty()) {
81             throw new IllegalArgumentException(INVALID_TOPIC_MSG);
82         }
83
84         if (eventProtocolParams.getEventClass() == null) {
85             throw new IllegalArgumentException("Invalid Event Class");
86         }
87
88         String key = this.codersKey(eventProtocolParams.getGroupId(), eventProtocolParams.getArtifactId(),
89                 eventProtocolParams.getTopic());
90         String reverseKey = this.reverseCodersKey(eventProtocolParams.getTopic(), eventProtocolParams.getEventClass());
91
92         synchronized (this) {
93             if (coders.containsKey(key)) {
94                 ProtocolCoderToolset toolset = coders.get(key);
95
96                 logger.info("{}: adding coders for existing {}: {}", this, key, toolset);
97
98                 toolset
99                         .addCoder(
100                                 eventProtocolParams.getEventClass(),
101                                 eventProtocolParams.getProtocolFilter(),
102                                 eventProtocolParams.getModelClassLoaderHash());
103
104                 if (!reverseCoders.containsKey(reverseKey)) {
105                     logger.info(
106                             "{}: adding new reverse coders (multiple classes case) for {}:{}: {}",
107                             this,
108                             reverseKey,
109                             key,
110                             toolset);
111
112                     List<ProtocolCoderToolset> reverseMappings =
113                             new ArrayList<>();
114                     reverseMappings.add(toolset);
115                     reverseCoders.put(reverseKey, reverseMappings);
116                 }
117                 return;
118             }
119
120             GsonProtocolCoderToolset coderTools =
121                     new GsonProtocolCoderToolset(eventProtocolParams, key);
122
123             logger.info("{}: adding coders for new {}: {}", this, key, coderTools);
124
125             coders.put(key, coderTools);
126
127             addReverseCoder(coderTools, key, reverseKey);
128         }
129     }
130
131     private void addReverseCoder(GsonProtocolCoderToolset coderTools, String key, String reverseKey) {
132         if (reverseCoders.containsKey(reverseKey)) {
133             // There is another controller (different group id/artifact id/topic)
134             // that shares the class and the topic.
135
136             List<ProtocolCoderToolset> toolsets =
137                     reverseCoders.get(reverseKey);
138             boolean present = false;
139             for (ProtocolCoderToolset parserSet : toolsets) {
140                 // just doublecheck
141                 present = parserSet.getControllerId().equals(key);
142                 if (present) {
143                     /* anomaly */
144                     logger.error(
145                             "{}: unexpected toolset reverse mapping found for {}:{}: {}",
146                             this,
147                             reverseKey,
148                             key,
149                             parserSet);
150                 }
151             }
152
153             if (present) {
154                 return;
155             } else {
156                 logger.info("{}: adding coder set for {}: {} ", this, reverseKey, coderTools);
157                 toolsets.add(coderTools);
158             }
159         } else {
160             List<ProtocolCoderToolset> toolsets = new ArrayList<>();
161             toolsets.add(coderTools);
162
163             logger.info("{}: adding toolset for reverse key {}: {}", this, reverseKey, toolsets);
164             reverseCoders.put(reverseKey, toolsets);
165         }
166     }
167
168     /**
169      * produces key for indexing toolset entries.
170      *
171      * @param groupId    group id
172      * @param artifactId artifact id
173      * @param topic      topic
174      * @return index key
175      */
176     protected String codersKey(String groupId, String artifactId, String topic) {
177         return groupId + ":" + artifactId + ":" + topic;
178     }
179
180     /**
181      * produces a key for the reverse index.
182      *
183      * @param topic      topic
184      * @param eventClass coded class
185      * @return reverse index key
186      */
187     protected String reverseCodersKey(String topic, String eventClass) {
188         return topic + ":" + eventClass;
189     }
190
191     /**
192      * remove coder.
193      *
194      * @param groupId    group id
195      * @param artifactId artifact id
196      * @param topic      topic
197      * @throws IllegalArgumentException if invalid input
198      */
199     public void remove(String groupId, String artifactId, String topic) {
200
201         if (groupId == null || groupId.isEmpty()) {
202             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
203         }
204
205         if (artifactId == null || artifactId.isEmpty()) {
206             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
207         }
208
209         if (topic == null || topic.isEmpty()) {
210             throw new IllegalArgumentException(INVALID_TOPIC_MSG);
211         }
212
213         String key = this.codersKey(groupId, artifactId, topic);
214
215         synchronized (this) {
216             if (coders.containsKey(key)) {
217                 ProtocolCoderToolset coderToolset = coders.remove(key);
218
219                 logger.info("{}: removed toolset for {}: {}", this, key, coderToolset);
220
221                 for (CoderFilters codeFilter : coderToolset.getCoders()) {
222                     String className = codeFilter.getCodedClass();
223                     String reverseKey = this.reverseCodersKey(topic, className);
224                     removeReverseCoder(key, reverseKey);
225                 }
226             }
227         }
228     }
229
230     private void removeReverseCoder(String key, String reverseKey) {
231         if (!this.reverseCoders.containsKey(reverseKey)) {
232             return;
233         }
234
235         List<ProtocolCoderToolset> toolsets =
236                 this.reverseCoders.get(reverseKey);
237         Iterator<ProtocolCoderToolset> toolsetsIter =
238                 toolsets.iterator();
239         while (toolsetsIter.hasNext()) {
240             ProtocolCoderToolset toolset = toolsetsIter.next();
241             if (toolset.getControllerId().equals(key)) {
242                 logger.info(
243                         "{}: removed coder from toolset for {} from reverse mapping", this, reverseKey);
244                 toolsetsIter.remove();
245             }
246         }
247
248         if (this.reverseCoders.get(reverseKey).isEmpty()) {
249             logger.info("{}: removing reverse mapping for {}: ", this, reverseKey);
250             this.reverseCoders.remove(reverseKey);
251         }
252     }
253
254     /**
255      * does it support coding.
256      *
257      * @param groupId    group id
258      * @param artifactId artifact id
259      * @param topic      topic
260      * @return true if its is codable
261      */
262     public boolean isCodingSupported(String groupId, String artifactId, String topic) {
263
264         if (groupId == null || groupId.isEmpty()) {
265             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
266         }
267
268         if (artifactId == null || artifactId.isEmpty()) {
269             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
270         }
271
272         if (topic == null || topic.isEmpty()) {
273             throw new IllegalArgumentException(INVALID_TOPIC_MSG);
274         }
275
276         String key = this.codersKey(groupId, artifactId, topic);
277         synchronized (this) {
278             return coders.containsKey(key);
279         }
280     }
281
282     /**
283      * decode a json string into an Object.
284      *
285      * @param groupId    group id
286      * @param artifactId artifact id
287      * @param topic      topic
288      * @param json       json string to convert to object
289      * @return the decoded object
290      * @throws IllegalArgumentException      if invalid argument is provided
291      * @throws UnsupportedOperationException if the operation cannot be performed
292      */
293     public Object decode(String groupId, String artifactId, String topic, String json) {
294
295         if (!isCodingSupported(groupId, artifactId, topic)) {
296             throw new IllegalArgumentException(
297                     UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic) + " for encoding");
298         }
299
300         String key = this.codersKey(groupId, artifactId, topic);
301         ProtocolCoderToolset coderTools = coders.get(key);
302         try {
303             Object event = coderTools.decode(json);
304             if (event != null) {
305                 return event;
306             }
307         } catch (Exception e) {
308             logger.debug("{}, cannot decode {}", this, json, e);
309         }
310
311         throw new UnsupportedOperationException("Cannot decode with gson");
312     }
313
314     /**
315      * encode an object into a json string.
316      *
317      * @param groupId    group id
318      * @param artifactId artifact id
319      * @param topic      topic
320      * @param event      object to convert to string
321      * @return the json string
322      * @throws IllegalArgumentException      if invalid argument is provided
323      * @throws UnsupportedOperationException if the operation cannot be performed
324      */
325     public String encode(String groupId, String artifactId, String topic, Object event) {
326
327         if (!isCodingSupported(groupId, artifactId, topic)) {
328             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
329         }
330
331         if (event == null) {
332             throw new IllegalArgumentException("Unsupported topic:" + topic);
333         }
334
335         // reuse the decoder set, since there must be affinity in the model
336         String key = this.codersKey(groupId, artifactId, topic);
337         return this.encodeInternal(key, event);
338     }
339
340     /**
341      * encode an object into a json string.
342      *
343      * @param topic topic
344      * @param event object to convert to string
345      * @return the json string
346      * @throws IllegalArgumentException      if invalid argument is provided
347      * @throws UnsupportedOperationException if the operation cannot be performed
348      */
349     public String encode(String topic, Object event) {
350
351         if (event == null) {
352             throw new IllegalArgumentException("Invalid encoded class");
353         }
354
355         if (topic == null || topic.isEmpty()) {
356             throw new IllegalArgumentException("Invalid topic");
357         }
358
359         String reverseKey = this.reverseCodersKey(topic, event.getClass().getName());
360         if (!this.reverseCoders.containsKey(reverseKey)) {
361             throw new IllegalArgumentException("no reverse coder has been found");
362         }
363
364         List<ProtocolCoderToolset> toolsets =
365                 this.reverseCoders.get(reverseKey);
366
367         String key =
368                 codersKey(
369                         toolsets.get(0).getGroupId(), toolsets.get(0).getArtifactId(), topic);
370         return this.encodeInternal(key, event);
371     }
372
373     /**
374      * encode an object into a json string.
375      *
376      * @param topic        topic
377      * @param encodedClass object to convert to string
378      * @return the json string
379      * @throws IllegalArgumentException      if invalid argument is provided
380      * @throws UnsupportedOperationException if the operation cannot be performed
381      */
382     public String encode(String topic, Object encodedClass, DroolsController droolsController) {
383
384         if (encodedClass == null) {
385             throw new IllegalArgumentException("Invalid encoded class");
386         }
387
388         if (topic == null || topic.isEmpty()) {
389             throw new IllegalArgumentException("Invalid topic");
390         }
391
392         String key = codersKey(droolsController.getGroupId(), droolsController.getArtifactId(), topic);
393         return this.encodeInternal(key, encodedClass);
394     }
395
396     /**
397      * encode an object into a json string.
398      *
399      * @param key   identifier
400      * @param event object to convert to string
401      * @return the json string
402      * @throws IllegalArgumentException      if invalid argument is provided
403      * @throws UnsupportedOperationException if the operation cannot be performed
404      */
405     protected String encodeInternal(String key, Object event) {
406
407         logger.debug("{}: encode for {}: {}", this, key, event);
408
409         ProtocolCoderToolset coderTools = coders.get(key);
410         try {
411             String json = coderTools.encode(event);
412             if (json != null && !json.isEmpty()) {
413                 return json;
414             }
415         } catch (Exception e) {
416             logger.warn("{}: cannot encode (first) for {}: {}", this, key, event, e);
417         }
418
419         throw new UnsupportedOperationException("Cannot decode with gson");
420     }
421
422     /**
423      * Drools creators.
424      *
425      * @param topic        topic
426      * @param encodedClass encoded class
427      * @return list of controllers
428      * @throws IllegalStateException    illegal state
429      * @throws IllegalArgumentException argument
430      */
431     protected List<DroolsController> droolsCreators(String topic, Object encodedClass) {
432
433         List<DroolsController> droolsControllers = new ArrayList<>();
434
435         String reverseKey = this.reverseCodersKey(topic, encodedClass.getClass().getName());
436         if (!this.reverseCoders.containsKey(reverseKey)) {
437             logger.warn("{}: no reverse mapping for {}", this, reverseKey);
438             return droolsControllers;
439         }
440
441         List<ProtocolCoderToolset> toolsets =
442                 this.reverseCoders.get(reverseKey);
443
444         // There must be multiple toolsets associated with <topic,classname> reverseKey
445         // case 2 different controllers use the same models and register the same encoder for
446         // the same topic.  This is assumed not to occur often but for the purpose of encoding
447         // but there should be no side-effects.  Ownership is crosscheck against classname and
448         // classloader reference.
449
450         if (toolsets == null || toolsets.isEmpty()) {
451             throw new IllegalStateException(
452                     "No Encoders toolsets available for topic "
453                             + topic
454                             + " encoder "
455                             + encodedClass.getClass().getName());
456         }
457
458         for (ProtocolCoderToolset encoderSet : toolsets) {
459             addToolsetControllers(droolsControllers, encodedClass, encoderSet);
460         }
461
462         if (droolsControllers.isEmpty()) {
463             throw new IllegalStateException(
464                     "No Encoders toolsets available for "
465                             + topic
466                             + ":"
467                             + encodedClass.getClass().getName());
468         }
469
470         return droolsControllers;
471     }
472
473     private void addToolsetControllers(List<DroolsController> droolsControllers, Object encodedClass,
474                     ProtocolCoderToolset encoderSet) {
475         // figure out the right toolset
476         String groupId = encoderSet.getGroupId();
477         String artifactId = encoderSet.getArtifactId();
478         List<CoderFilters> coderFilters = encoderSet.getCoders();
479         for (CoderFilters coder : coderFilters) {
480             if (coder.getCodedClass().equals(encodedClass.getClass().getName())) {
481                 DroolsController droolsController =
482                                 DroolsControllerConstants.getFactory().get(groupId, artifactId, "");
483                 if (droolsController.ownsCoder(
484                         encodedClass.getClass(), coder.getModelClassLoaderHash())) {
485                     droolsControllers.add(droolsController);
486                 }
487             }
488         }
489     }
490
491     /**
492      * get all filters by maven coordinates and topic.
493      *
494      * @param groupId    group id
495      * @param artifactId artifact id
496      * @param topic      topic
497      * @return list of coders
498      * @throws IllegalArgumentException if invalid input
499      */
500     public List<CoderFilters> getFilters(String groupId, String artifactId, String topic) {
501
502         if (!isCodingSupported(groupId, artifactId, topic)) {
503             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
504         }
505
506         String key = this.codersKey(groupId, artifactId, topic);
507         ProtocolCoderToolset coderTools = coders.get(key);
508         return coderTools.getCoders();
509     }
510
511     /**
512      * get all coders by maven coordinates and topic.
513      *
514      * @param groupId    group id
515      * @param artifactId artifact id
516      * @return list of coders
517      * @throws IllegalArgumentException if invalid input
518      */
519     public List<CoderFilters> getFilters(String groupId, String artifactId) {
520
521         if (groupId == null || groupId.isEmpty()) {
522             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
523         }
524
525         if (artifactId == null || artifactId.isEmpty()) {
526             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
527         }
528
529         String key = this.codersKey(groupId, artifactId, "");
530
531         List<CoderFilters> codersFilters = new ArrayList<>();
532         for (Map.Entry<String, ProtocolCoderToolset> entry :
533                 coders.entrySet()) {
534             if (entry.getKey().startsWith(key)) {
535                 codersFilters.addAll(entry.getValue().getCoders());
536             }
537         }
538
539         return codersFilters;
540     }
541
542     /**
543      * get all filters by maven coordinates, topic, and classname.
544      *
545      * @param groupId    group id
546      * @param artifactId artifact id
547      * @param topic      topic
548      * @param classname  classname
549      * @return list of coders
550      * @throws IllegalArgumentException if invalid input
551      */
552     public CoderFilters getFilters(
553             String groupId, String artifactId, String topic, String classname) {
554
555         if (!isCodingSupported(groupId, artifactId, topic)) {
556             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
557         }
558
559         if (classname == null || classname.isEmpty()) {
560             throw new IllegalArgumentException("classname must be provided");
561         }
562
563         String key = this.codersKey(groupId, artifactId, topic);
564         ProtocolCoderToolset coderTools = coders.get(key);
565         return coderTools.getCoder(classname);
566     }
567
568     /**
569      * get all coders by maven coordinates and topic.
570      *
571      * @param groupId    group id
572      * @param artifactId artifact id
573      * @param topic      topic
574      * @return list of coders
575      * @throws IllegalArgumentException if invalid input
576      */
577     public ProtocolCoderToolset getCoders(
578             String groupId, String artifactId, String topic) {
579
580         if (!isCodingSupported(groupId, artifactId, topic)) {
581             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
582         }
583
584         String key = this.codersKey(groupId, artifactId, topic);
585         return coders.get(key);
586     }
587
588     /**
589      * get all coders by maven coordinates and topic.
590      *
591      * @param groupId    group id
592      * @param artifactId artifact id
593      * @return list of coders
594      * @throws IllegalArgumentException if invalid input
595      */
596     public List<ProtocolCoderToolset> getCoders(
597             String groupId, String artifactId) {
598
599         if (groupId == null || groupId.isEmpty()) {
600             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
601         }
602
603         if (artifactId == null || artifactId.isEmpty()) {
604             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
605         }
606
607         String key = this.codersKey(groupId, artifactId, "");
608
609         List<ProtocolCoderToolset> coderToolset = new ArrayList<>();
610         for (Map.Entry<String, ProtocolCoderToolset> entry :
611                 coders.entrySet()) {
612             if (entry.getKey().startsWith(key)) {
613                 coderToolset.add(entry.getValue());
614             }
615         }
616
617         return coderToolset;
618     }
619
620     /**
621      * get coded based on class and topic.
622      *
623      * @param topic      topic
624      * @param codedClass class
625      * @return list of reverse filters
626      */
627     public List<CoderFilters> getReverseFilters(String topic, String codedClass) {
628
629         if (topic == null || topic.isEmpty()) {
630             throw new IllegalArgumentException(UNSUPPORTED_MSG);
631         }
632
633         if (codedClass == null) {
634             throw new IllegalArgumentException(MISSING_CLASS);
635         }
636
637         String key = this.reverseCodersKey(topic, codedClass);
638         List<ProtocolCoderToolset> toolsets = this.reverseCoders.get(key);
639         if (toolsets == null) {
640             throw new IllegalArgumentException("No Coder found for " + key);
641         }
642
643         List<CoderFilters> coderFilters = new ArrayList<>();
644         for (ProtocolCoderToolset toolset : toolsets) {
645             coderFilters.addAll(toolset.getCoders());
646         }
647
648         return coderFilters;
649     }
650
651     /**
652      * returns group and artifact id of the creator of the encoder.
653      *
654      * @param topic topic
655      * @param fact  fact
656      * @return the drools controller
657      */
658     DroolsController getDroolsController(String topic, Object fact) {
659
660         if (topic == null || topic.isEmpty()) {
661             throw new IllegalArgumentException(UNSUPPORTED_MSG);
662         }
663
664         if (fact == null) {
665             throw new IllegalArgumentException(MISSING_CLASS);
666         }
667
668         List<DroolsController> droolsControllers = droolsCreators(topic, fact);
669
670         if (droolsControllers.isEmpty()) {
671             throw new IllegalArgumentException("Invalid Topic: " + topic);
672         }
673
674         if (droolsControllers.size() > 1) {
675             logger.warn(
676                     "{}: multiple drools-controller {} for {}:{} ",
677                     this,
678                     droolsControllers,
679                     topic,
680                     fact.getClass().getName());
681             // continue
682         }
683         return droolsControllers.get(0);
684     }
685
686     /**
687      * returns group and artifact id of the creator of the encoder.
688      *
689      * @param topic topic
690      * @param fact  fact
691      * @return list of drools controllers
692      */
693     List<DroolsController> getDroolsControllers(String topic, Object fact) {
694
695         if (topic == null || topic.isEmpty()) {
696             throw new IllegalArgumentException(UNSUPPORTED_MSG);
697         }
698
699         if (fact == null) {
700             throw new IllegalArgumentException(MISSING_CLASS);
701         }
702
703         List<DroolsController> droolsControllers = droolsCreators(topic, fact);
704         if (droolsControllers.size() > 1) {
705             // unexpected
706             logger.warn(
707                     "{}: multiple drools-controller {} for {}:{} ",
708                     this,
709                     droolsControllers,
710                     topic,
711                     fact.getClass().getName());
712             // continue
713         }
714         return droolsControllers;
715     }
716
717     @Override
718     public String toString() {
719         return "GenericEventProtocolCoder [coders="
720                 + coders.keySet()
721                 + ", reverseCoders="
722                 + reverseCoders.keySet()
723                 + "]";
724     }
725 }