b16186d6314deb8d1f9f4e0e86b509b4373f2977
[policy/drools-pdp.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2019-2020 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                 logger.info("{}: adding coder set for {}: {} ", this, reverseKey, coderTools);
155                 toolsets.add(coderTools);
156             }
157         } else {
158             List<ProtocolCoderToolset> toolsets = new ArrayList<>();
159             toolsets.add(coderTools);
160
161             logger.info("{}: adding toolset for reverse key {}: {}", this, reverseKey, toolsets);
162             reverseCoders.put(reverseKey, toolsets);
163         }
164     }
165
166     /**
167      * produces key for indexing toolset entries.
168      *
169      * @param groupId    group id
170      * @param artifactId artifact id
171      * @param topic      topic
172      * @return index key
173      */
174     protected String codersKey(String groupId, String artifactId, String topic) {
175         return groupId + ":" + artifactId + ":" + topic;
176     }
177
178     /**
179      * produces a key for the reverse index.
180      *
181      * @param topic      topic
182      * @param eventClass coded class
183      * @return reverse index key
184      */
185     protected String reverseCodersKey(String topic, String eventClass) {
186         return topic + ":" + eventClass;
187     }
188
189     /**
190      * remove coder.
191      *
192      * @param groupId    group id
193      * @param artifactId artifact id
194      * @param topic      topic
195      * @throws IllegalArgumentException if invalid input
196      */
197     public void remove(String groupId, String artifactId, String topic) {
198
199         if (groupId == null || groupId.isEmpty()) {
200             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
201         }
202
203         if (artifactId == null || artifactId.isEmpty()) {
204             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
205         }
206
207         if (topic == null || topic.isEmpty()) {
208             throw new IllegalArgumentException(INVALID_TOPIC_MSG);
209         }
210
211         String key = this.codersKey(groupId, artifactId, topic);
212
213         synchronized (this) {
214             if (coders.containsKey(key)) {
215                 ProtocolCoderToolset coderToolset = coders.remove(key);
216
217                 logger.info("{}: removed toolset for {}: {}", this, key, coderToolset);
218
219                 for (CoderFilters codeFilter : coderToolset.getCoders()) {
220                     String className = codeFilter.getCodedClass();
221                     String reverseKey = this.reverseCodersKey(topic, className);
222                     removeReverseCoder(key, reverseKey);
223                 }
224             }
225         }
226     }
227
228     private void removeReverseCoder(String key, String reverseKey) {
229         if (!this.reverseCoders.containsKey(reverseKey)) {
230             return;
231         }
232
233         List<ProtocolCoderToolset> toolsets =
234                 this.reverseCoders.get(reverseKey);
235         Iterator<ProtocolCoderToolset> toolsetsIter =
236                 toolsets.iterator();
237         while (toolsetsIter.hasNext()) {
238             ProtocolCoderToolset toolset = toolsetsIter.next();
239             if (toolset.getControllerId().equals(key)) {
240                 logger.info(
241                         "{}: removed coder from toolset for {} from reverse mapping", this, reverseKey);
242                 toolsetsIter.remove();
243             }
244         }
245
246         if (this.reverseCoders.get(reverseKey).isEmpty()) {
247             logger.info("{}: removing reverse mapping for {}: ", this, reverseKey);
248             this.reverseCoders.remove(reverseKey);
249         }
250     }
251
252     /**
253      * does it support coding.
254      *
255      * @param groupId    group id
256      * @param artifactId artifact id
257      * @param topic      topic
258      * @return true if its is codable
259      */
260     public boolean isCodingSupported(String groupId, String artifactId, String topic) {
261
262         if (groupId == null || groupId.isEmpty()) {
263             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
264         }
265
266         if (artifactId == null || artifactId.isEmpty()) {
267             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
268         }
269
270         if (topic == null || topic.isEmpty()) {
271             throw new IllegalArgumentException(INVALID_TOPIC_MSG);
272         }
273
274         String key = this.codersKey(groupId, artifactId, topic);
275         synchronized (this) {
276             return coders.containsKey(key);
277         }
278     }
279
280     /**
281      * decode a json string into an Object.
282      *
283      * @param groupId    group id
284      * @param artifactId artifact id
285      * @param topic      topic
286      * @param json       json string to convert to object
287      * @return the decoded object
288      * @throws IllegalArgumentException      if invalid argument is provided
289      * @throws UnsupportedOperationException if the operation cannot be performed
290      */
291     public Object decode(String groupId, String artifactId, String topic, String json) {
292
293         if (!isCodingSupported(groupId, artifactId, topic)) {
294             throw new IllegalArgumentException(
295                     UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic) + " for encoding");
296         }
297
298         String key = this.codersKey(groupId, artifactId, topic);
299         ProtocolCoderToolset coderTools = coders.get(key);
300         try {
301             Object event = coderTools.decode(json);
302             if (event != null) {
303                 return event;
304             }
305         } catch (Exception e) {
306             logger.debug("{}, cannot decode {}", this, json, e);
307         }
308
309         throw new UnsupportedOperationException("Cannot decode with gson");
310     }
311
312     /**
313      * encode an object into a json string.
314      *
315      * @param groupId    group id
316      * @param artifactId artifact id
317      * @param topic      topic
318      * @param event      object to convert to string
319      * @return the json string
320      * @throws IllegalArgumentException      if invalid argument is provided
321      * @throws UnsupportedOperationException if the operation cannot be performed
322      */
323     public String encode(String groupId, String artifactId, String topic, Object event) {
324
325         if (!isCodingSupported(groupId, artifactId, topic)) {
326             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
327         }
328
329         if (event == null) {
330             throw new IllegalArgumentException("Unsupported topic:" + topic);
331         }
332
333         // reuse the decoder set, since there must be affinity in the model
334         String key = this.codersKey(groupId, artifactId, topic);
335         return this.encodeInternal(key, event);
336     }
337
338     /**
339      * encode an object into a json string.
340      *
341      * @param topic topic
342      * @param event object to convert to string
343      * @return the json string
344      * @throws IllegalArgumentException      if invalid argument is provided
345      * @throws UnsupportedOperationException if the operation cannot be performed
346      */
347     public String encode(String topic, Object event) {
348
349         if (event == null) {
350             throw new IllegalArgumentException("Invalid encoded class");
351         }
352
353         if (topic == null || topic.isEmpty()) {
354             throw new IllegalArgumentException("Invalid topic");
355         }
356
357         String reverseKey = this.reverseCodersKey(topic, event.getClass().getName());
358         if (!this.reverseCoders.containsKey(reverseKey)) {
359             throw new IllegalArgumentException("no reverse coder has been found");
360         }
361
362         List<ProtocolCoderToolset> toolsets =
363                 this.reverseCoders.get(reverseKey);
364
365         String key =
366                 codersKey(
367                         toolsets.get(0).getGroupId(), toolsets.get(0).getArtifactId(), topic);
368         return this.encodeInternal(key, event);
369     }
370
371     /**
372      * encode an object into a json string.
373      *
374      * @param topic        topic
375      * @param encodedClass object to convert to string
376      * @return the json string
377      * @throws IllegalArgumentException      if invalid argument is provided
378      * @throws UnsupportedOperationException if the operation cannot be performed
379      */
380     public String encode(String topic, Object encodedClass, DroolsController droolsController) {
381
382         if (encodedClass == null) {
383             throw new IllegalArgumentException("Invalid encoded class");
384         }
385
386         if (topic == null || topic.isEmpty()) {
387             throw new IllegalArgumentException("Invalid topic");
388         }
389
390         String key = codersKey(droolsController.getGroupId(), droolsController.getArtifactId(), topic);
391         return this.encodeInternal(key, encodedClass);
392     }
393
394     /**
395      * encode an object into a json string.
396      *
397      * @param key   identifier
398      * @param event object to convert to string
399      * @return the json string
400      * @throws IllegalArgumentException      if invalid argument is provided
401      * @throws UnsupportedOperationException if the operation cannot be performed
402      */
403     protected String encodeInternal(String key, Object event) {
404
405         logger.debug("{}: encode for {}: {}", this, key, event);
406
407         ProtocolCoderToolset coderTools = coders.get(key);
408         try {
409             String json = coderTools.encode(event);
410             if (json != null && !json.isEmpty()) {
411                 return json;
412             }
413         } catch (Exception e) {
414             logger.warn("{}: cannot encode (first) for {}: {}", this, key, event, e);
415         }
416
417         throw new UnsupportedOperationException("Cannot decode with gson");
418     }
419
420     /**
421      * Drools creators.
422      *
423      * @param topic        topic
424      * @param encodedClass encoded class
425      * @return list of controllers
426      * @throws IllegalStateException    illegal state
427      * @throws IllegalArgumentException argument
428      */
429     protected List<DroolsController> droolsCreators(String topic, Object encodedClass) {
430
431         List<DroolsController> droolsControllers = new ArrayList<>();
432
433         String reverseKey = this.reverseCodersKey(topic, encodedClass.getClass().getName());
434         if (!this.reverseCoders.containsKey(reverseKey)) {
435             logger.warn("{}: no reverse mapping for {}", this, reverseKey);
436             return droolsControllers;
437         }
438
439         List<ProtocolCoderToolset> toolsets =
440                 this.reverseCoders.get(reverseKey);
441
442         // There must be multiple toolsets associated with <topic,classname> reverseKey
443         // case 2 different controllers use the same models and register the same encoder for
444         // the same topic.  This is assumed not to occur often but for the purpose of encoding
445         // but there should be no side-effects.  Ownership is crosscheck against classname and
446         // classloader reference.
447
448         if (toolsets == null || toolsets.isEmpty()) {
449             throw new IllegalStateException(
450                     "No Encoders toolsets available for topic "
451                             + topic
452                             + " encoder "
453                             + encodedClass.getClass().getName());
454         }
455
456         for (ProtocolCoderToolset encoderSet : toolsets) {
457             addToolsetControllers(droolsControllers, encodedClass, encoderSet);
458         }
459
460         if (droolsControllers.isEmpty()) {
461             throw new IllegalStateException(
462                     "No Encoders toolsets available for "
463                             + topic
464                             + ":"
465                             + encodedClass.getClass().getName());
466         }
467
468         return droolsControllers;
469     }
470
471     private void addToolsetControllers(List<DroolsController> droolsControllers, Object encodedClass,
472                     ProtocolCoderToolset encoderSet) {
473         // figure out the right toolset
474         String groupId = encoderSet.getGroupId();
475         String artifactId = encoderSet.getArtifactId();
476         List<CoderFilters> coderFilters = encoderSet.getCoders();
477         for (CoderFilters coder : coderFilters) {
478             if (coder.getCodedClass().equals(encodedClass.getClass().getName())) {
479                 DroolsController droolsController =
480                                 DroolsControllerConstants.getFactory().get(groupId, artifactId, "");
481                 if (droolsController.ownsCoder(
482                         encodedClass.getClass(), coder.getModelClassLoaderHash())) {
483                     droolsControllers.add(droolsController);
484                 }
485             }
486         }
487     }
488
489     /**
490      * get all filters by maven coordinates and topic.
491      *
492      * @param groupId    group id
493      * @param artifactId artifact id
494      * @param topic      topic
495      * @return list of coders
496      * @throws IllegalArgumentException if invalid input
497      */
498     public List<CoderFilters> getFilters(String groupId, String artifactId, String topic) {
499
500         if (!isCodingSupported(groupId, artifactId, topic)) {
501             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
502         }
503
504         String key = this.codersKey(groupId, artifactId, topic);
505         ProtocolCoderToolset coderTools = coders.get(key);
506         return coderTools.getCoders();
507     }
508
509     /**
510      * get all coders by maven coordinates and topic.
511      *
512      * @param groupId    group id
513      * @param artifactId artifact id
514      * @return list of coders
515      * @throws IllegalArgumentException if invalid input
516      */
517     public List<CoderFilters> getFilters(String groupId, String artifactId) {
518
519         if (groupId == null || groupId.isEmpty()) {
520             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
521         }
522
523         if (artifactId == null || artifactId.isEmpty()) {
524             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
525         }
526
527         String key = this.codersKey(groupId, artifactId, "");
528
529         List<CoderFilters> codersFilters = new ArrayList<>();
530         for (Map.Entry<String, ProtocolCoderToolset> entry :
531                 coders.entrySet()) {
532             if (entry.getKey().startsWith(key)) {
533                 codersFilters.addAll(entry.getValue().getCoders());
534             }
535         }
536
537         return codersFilters;
538     }
539
540     /**
541      * get all filters by maven coordinates, topic, and classname.
542      *
543      * @param groupId    group id
544      * @param artifactId artifact id
545      * @param topic      topic
546      * @param classname  classname
547      * @return list of coders
548      * @throws IllegalArgumentException if invalid input
549      */
550     public CoderFilters getFilters(
551             String groupId, String artifactId, String topic, String classname) {
552
553         if (!isCodingSupported(groupId, artifactId, topic)) {
554             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
555         }
556
557         if (classname == null || classname.isEmpty()) {
558             throw new IllegalArgumentException("classname must be provided");
559         }
560
561         String key = this.codersKey(groupId, artifactId, topic);
562         ProtocolCoderToolset coderTools = coders.get(key);
563         return coderTools.getCoder(classname);
564     }
565
566     /**
567      * get all coders by maven coordinates and topic.
568      *
569      * @param groupId    group id
570      * @param artifactId artifact id
571      * @param topic      topic
572      * @return list of coders
573      * @throws IllegalArgumentException if invalid input
574      */
575     public ProtocolCoderToolset getCoders(
576             String groupId, String artifactId, String topic) {
577
578         if (!isCodingSupported(groupId, artifactId, topic)) {
579             throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
580         }
581
582         String key = this.codersKey(groupId, artifactId, topic);
583         return coders.get(key);
584     }
585
586     /**
587      * get all coders by maven coordinates and topic.
588      *
589      * @param groupId    group id
590      * @param artifactId artifact id
591      * @return list of coders
592      * @throws IllegalArgumentException if invalid input
593      */
594     public List<ProtocolCoderToolset> getCoders(
595             String groupId, String artifactId) {
596
597         if (groupId == null || groupId.isEmpty()) {
598             throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
599         }
600
601         if (artifactId == null || artifactId.isEmpty()) {
602             throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
603         }
604
605         String key = this.codersKey(groupId, artifactId, "");
606
607         List<ProtocolCoderToolset> coderToolset = new ArrayList<>();
608         for (Map.Entry<String, ProtocolCoderToolset> entry :
609                 coders.entrySet()) {
610             if (entry.getKey().startsWith(key)) {
611                 coderToolset.add(entry.getValue());
612             }
613         }
614
615         return coderToolset;
616     }
617
618     /**
619      * get coded based on class and topic.
620      *
621      * @param topic      topic
622      * @param codedClass class
623      * @return list of reverse filters
624      */
625     public List<CoderFilters> getReverseFilters(String topic, String codedClass) {
626
627         if (topic == null || topic.isEmpty()) {
628             throw new IllegalArgumentException(UNSUPPORTED_MSG);
629         }
630
631         if (codedClass == null) {
632             throw new IllegalArgumentException(MISSING_CLASS);
633         }
634
635         String key = this.reverseCodersKey(topic, codedClass);
636         List<ProtocolCoderToolset> toolsets = this.reverseCoders.get(key);
637         if (toolsets == null) {
638             throw new IllegalArgumentException("No Coder found for " + key);
639         }
640
641         List<CoderFilters> coderFilters = new ArrayList<>();
642         for (ProtocolCoderToolset toolset : toolsets) {
643             coderFilters.addAll(toolset.getCoders());
644         }
645
646         return coderFilters;
647     }
648
649     /**
650      * returns group and artifact id of the creator of the encoder.
651      *
652      * @param topic topic
653      * @param fact  fact
654      * @return the drools controller
655      */
656     DroolsController getDroolsController(String topic, Object fact) {
657
658         if (topic == null || topic.isEmpty()) {
659             throw new IllegalArgumentException(UNSUPPORTED_MSG);
660         }
661
662         if (fact == null) {
663             throw new IllegalArgumentException(MISSING_CLASS);
664         }
665
666         List<DroolsController> droolsControllers = droolsCreators(topic, fact);
667
668         if (droolsControllers.isEmpty()) {
669             throw new IllegalArgumentException("Invalid Topic: " + topic);
670         }
671
672         if (droolsControllers.size() > 1) {
673             logger.warn(
674                     "{}: multiple drools-controller {} for {}:{} ",
675                     this,
676                     droolsControllers,
677                     topic,
678                     fact.getClass().getName());
679             // continue
680         }
681         return droolsControllers.get(0);
682     }
683
684     /**
685      * returns group and artifact id of the creator of the encoder.
686      *
687      * @param topic topic
688      * @param fact  fact
689      * @return list of drools controllers
690      */
691     List<DroolsController> getDroolsControllers(String topic, Object fact) {
692
693         if (topic == null || topic.isEmpty()) {
694             throw new IllegalArgumentException(UNSUPPORTED_MSG);
695         }
696
697         if (fact == null) {
698             throw new IllegalArgumentException(MISSING_CLASS);
699         }
700
701         List<DroolsController> droolsControllers = droolsCreators(topic, fact);
702         if (droolsControllers.size() > 1) {
703             // unexpected
704             logger.warn(
705                     "{}: multiple drools-controller {} for {}:{} ",
706                     this,
707                     droolsControllers,
708                     topic,
709                     fact.getClass().getName());
710             // continue
711         }
712         return droolsControllers;
713     }
714
715     @Override
716     public String toString() {
717         return "GenericEventProtocolCoder [coders="
718                 + coders.keySet()
719                 + ", reverseCoders="
720                 + reverseCoders.keySet()
721                 + "]";
722     }
723 }