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