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