2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2019-2020, 2021 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
11 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
21 package org.onap.policy.drools.protocol.coders;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.List;
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;
35 * This protocol Coder that does its best attempt to decode/encode, selecting the best class and best fitted json
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";
46 private static final Logger logger = LoggerFactory.getLogger(GenericEventProtocolCoder.class);
49 * Mapping topic:controller-id -> /<protocol-decoder-toolset/> where protocol-coder-toolset contains
50 * a gson-protocol-coder-toolset.
52 protected final HashMap<String, ProtocolCoderToolset> coders =
56 * Mapping topic + classname -> Protocol Set.
58 protected final HashMap<String, List<ProtocolCoderToolset>>
59 reverseCoders = new HashMap<>();
61 GenericEventProtocolCoder() {
68 public void add(EventProtocolParams eventProtocolParams) {
69 if (eventProtocolParams.getGroupId() == null || eventProtocolParams.getGroupId().isEmpty()) {
70 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
73 if (eventProtocolParams.getArtifactId() == null || eventProtocolParams.getArtifactId().isEmpty()) {
74 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
77 if (eventProtocolParams.getTopic() == null || eventProtocolParams.getTopic().isEmpty()) {
78 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
81 if (eventProtocolParams.getEventClass() == null) {
82 throw new IllegalArgumentException("Invalid Event Class");
85 String key = this.codersKey(eventProtocolParams.getGroupId(), eventProtocolParams.getArtifactId(),
86 eventProtocolParams.getTopic());
87 String reverseKey = this.reverseCodersKey(eventProtocolParams.getTopic(), eventProtocolParams.getEventClass());
90 if (coders.containsKey(key)) {
91 ProtocolCoderToolset toolset = coders.get(key);
93 logger.info("{}: adding coders for existing {}: {}", this, key, toolset);
97 eventProtocolParams.getEventClass(),
98 eventProtocolParams.getProtocolFilter(),
99 eventProtocolParams.getModelClassLoaderHash());
101 if (!reverseCoders.containsKey(reverseKey)) {
103 "{}: adding new reverse coders (multiple classes case) for {}:{}: {}",
109 List<ProtocolCoderToolset> reverseMappings =
111 reverseMappings.add(toolset);
112 reverseCoders.put(reverseKey, reverseMappings);
117 var coderTools = new GsonProtocolCoderToolset(eventProtocolParams, key);
119 logger.info("{}: adding coders for new {}: {}", this, key, coderTools);
121 coders.put(key, coderTools);
123 addReverseCoder(coderTools, key, reverseKey);
127 private void addReverseCoder(GsonProtocolCoderToolset coderTools, String key, String reverseKey) {
128 if (reverseCoders.containsKey(reverseKey)) {
129 // There is another controller (different group id/artifact id/topic)
130 // that shares the class and the topic.
132 List<ProtocolCoderToolset> toolsets =
133 reverseCoders.get(reverseKey);
135 for (ProtocolCoderToolset parserSet : toolsets) {
137 present = parserSet.getControllerId().equals(key);
141 "{}: unexpected toolset reverse mapping found for {}:{}: {}",
150 logger.info("{}: adding coder set for {}: {} ", this, reverseKey, coderTools);
151 toolsets.add(coderTools);
154 List<ProtocolCoderToolset> toolsets = new ArrayList<>();
155 toolsets.add(coderTools);
157 logger.info("{}: adding toolset for reverse key {}: {}", this, reverseKey, toolsets);
158 reverseCoders.put(reverseKey, toolsets);
163 * produces key for indexing toolset entries.
165 * @param groupId group id
166 * @param artifactId artifact id
170 protected String codersKey(String groupId, String artifactId, String topic) {
171 return groupId + ":" + artifactId + ":" + topic;
175 * produces a key for the reverse index.
178 * @param eventClass coded class
179 * @return reverse index key
181 protected String reverseCodersKey(String topic, String eventClass) {
182 return topic + ":" + eventClass;
188 * @param groupId group id
189 * @param artifactId artifact id
191 * @throws IllegalArgumentException if invalid input
193 public void remove(String groupId, String artifactId, String topic) {
195 if (groupId == null || groupId.isEmpty()) {
196 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
199 if (artifactId == null || artifactId.isEmpty()) {
200 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
203 if (topic == null || topic.isEmpty()) {
204 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
207 String key = this.codersKey(groupId, artifactId, topic);
209 synchronized (this) {
210 if (coders.containsKey(key)) {
211 ProtocolCoderToolset coderToolset = coders.remove(key);
213 logger.info("{}: removed toolset for {}: {}", this, key, coderToolset);
215 for (CoderFilters codeFilter : coderToolset.getCoders()) {
216 String className = codeFilter.getCodedClass();
217 String reverseKey = this.reverseCodersKey(topic, className);
218 removeReverseCoder(key, reverseKey);
224 private void removeReverseCoder(String key, String reverseKey) {
225 if (!this.reverseCoders.containsKey(reverseKey)) {
229 List<ProtocolCoderToolset> toolsets =
230 this.reverseCoders.get(reverseKey);
231 Iterator<ProtocolCoderToolset> toolsetsIter =
233 while (toolsetsIter.hasNext()) {
234 ProtocolCoderToolset toolset = toolsetsIter.next();
235 if (toolset.getControllerId().equals(key)) {
237 "{}: removed coder from toolset for {} from reverse mapping", this, reverseKey);
238 toolsetsIter.remove();
242 if (this.reverseCoders.get(reverseKey).isEmpty()) {
243 logger.info("{}: removing reverse mapping for {}: ", this, reverseKey);
244 this.reverseCoders.remove(reverseKey);
249 * does it support coding.
251 * @param groupId group id
252 * @param artifactId artifact id
254 * @return true if its is codable
256 public boolean isCodingSupported(String groupId, String artifactId, String topic) {
258 if (groupId == null || groupId.isEmpty()) {
259 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
262 if (artifactId == null || artifactId.isEmpty()) {
263 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
266 if (topic == null || topic.isEmpty()) {
267 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
270 String key = this.codersKey(groupId, artifactId, topic);
271 synchronized (this) {
272 return coders.containsKey(key);
277 * decode a json string into an Object.
279 * @param groupId group id
280 * @param artifactId artifact id
282 * @param json json string to convert to object
283 * @return the decoded object
284 * @throws IllegalArgumentException if invalid argument is provided
285 * @throws UnsupportedOperationException if the operation cannot be performed
287 public Object decode(String groupId, String artifactId, String topic, String json) {
289 if (!isCodingSupported(groupId, artifactId, topic)) {
290 throw new IllegalArgumentException(
291 UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic) + " for encoding");
294 String key = this.codersKey(groupId, artifactId, topic);
295 ProtocolCoderToolset coderTools = coders.get(key);
297 Object event = coderTools.decode(json);
301 } catch (Exception e) {
302 logger.debug("{}, cannot decode {}", this, json, e);
305 throw new UnsupportedOperationException("Cannot decode with gson");
309 * encode an object into a json string.
311 * @param groupId group id
312 * @param artifactId artifact id
314 * @param event object to convert to string
315 * @return the json string
316 * @throws IllegalArgumentException if invalid argument is provided
317 * @throws UnsupportedOperationException if the operation cannot be performed
319 public String encode(String groupId, String artifactId, String topic, Object event) {
321 if (!isCodingSupported(groupId, artifactId, topic)) {
322 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
326 throw new IllegalArgumentException("Unsupported topic:" + topic);
329 // reuse the decoder set, since there must be affinity in the model
330 String key = this.codersKey(groupId, artifactId, topic);
331 return this.encodeInternal(key, event);
335 * encode an object into a json string.
338 * @param event object to convert to string
339 * @return the json string
340 * @throws IllegalArgumentException if invalid argument is provided
341 * @throws UnsupportedOperationException if the operation cannot be performed
343 public String encode(String topic, Object event) {
346 throw new IllegalArgumentException("Invalid encoded class");
349 if (topic == null || topic.isEmpty()) {
350 throw new IllegalArgumentException("Invalid topic");
353 String reverseKey = this.reverseCodersKey(topic, event.getClass().getName());
354 if (!this.reverseCoders.containsKey(reverseKey)) {
355 throw new IllegalArgumentException("no reverse coder has been found");
358 List<ProtocolCoderToolset> toolsets =
359 this.reverseCoders.get(reverseKey);
363 toolsets.get(0).getGroupId(), toolsets.get(0).getArtifactId(), topic);
364 return this.encodeInternal(key, event);
368 * encode an object into a json string.
371 * @param encodedClass object to convert to string
372 * @return the json string
373 * @throws IllegalArgumentException if invalid argument is provided
374 * @throws UnsupportedOperationException if the operation cannot be performed
376 public String encode(String topic, Object encodedClass, DroolsController droolsController) {
378 if (encodedClass == null) {
379 throw new IllegalArgumentException("Invalid encoded class");
382 if (topic == null || topic.isEmpty()) {
383 throw new IllegalArgumentException("Invalid topic");
386 String key = codersKey(droolsController.getGroupId(), droolsController.getArtifactId(), topic);
387 return this.encodeInternal(key, encodedClass);
391 * encode an object into a json string.
393 * @param key identifier
394 * @param event object to convert to string
395 * @return the json string
396 * @throws IllegalArgumentException if invalid argument is provided
397 * @throws UnsupportedOperationException if the operation cannot be performed
399 protected String encodeInternal(String key, Object event) {
401 logger.debug("{}: encode for {}: {}", this, key, event); // NOSONAR
404 * It seems that sonar declares the previous logging line as a security vulnerability
405 * when logging the topic variable. The static code analysis indicates that
406 * the path starts in org.onap.policy.drools.server.restful.RestManager::decode(),
407 * but the request is rejected if the topic contains invalid characters (the sonar description
408 * mentions "/r/n/t" characters) all of which are validated against in the checkValidNameInput(topic).
409 * Furthermore production instances are assumed not to have debug enabled, nor the REST telemetry API
410 * should be published externally. An additional note is that Path URLs containing spaces and newlines
411 * will be rejected earlier in the HTTP protocol libraries (jetty) so an URL of the form
412 * "https://../to\npic" won't even make it here.
415 ProtocolCoderToolset coderTools = coders.get(key);
417 String json = coderTools.encode(event);
418 if (json != null && !json.isEmpty()) {
421 } catch (Exception e) {
422 logger.warn("{}: cannot encode (first) for {}: {}", this, key, event, e);
425 throw new UnsupportedOperationException("Cannot decode with gson");
432 * @param encodedClass encoded class
433 * @return list of controllers
434 * @throws IllegalStateException illegal state
435 * @throws IllegalArgumentException argument
437 protected List<DroolsController> droolsCreators(String topic, Object encodedClass) {
439 List<DroolsController> droolsControllers = new ArrayList<>();
441 String reverseKey = this.reverseCodersKey(topic, encodedClass.getClass().getName());
442 if (!this.reverseCoders.containsKey(reverseKey)) {
443 logger.warn("{}: no reverse mapping for {}", this, reverseKey);
444 return droolsControllers;
447 List<ProtocolCoderToolset> toolsets =
448 this.reverseCoders.get(reverseKey);
450 // There must be multiple toolsets associated with <topic,classname> reverseKey
451 // case 2 different controllers use the same models and register the same encoder for
452 // the same topic. This is assumed not to occur often but for the purpose of encoding
453 // but there should be no side-effects. Ownership is crosscheck against classname and
454 // classloader reference.
456 if (toolsets == null || toolsets.isEmpty()) {
457 throw new IllegalStateException(
458 "No Encoders toolsets available for topic "
461 + encodedClass.getClass().getName());
464 for (ProtocolCoderToolset encoderSet : toolsets) {
465 addToolsetControllers(droolsControllers, encodedClass, encoderSet);
468 if (droolsControllers.isEmpty()) {
469 throw new IllegalStateException(
470 "No Encoders toolsets available for "
473 + encodedClass.getClass().getName());
476 return droolsControllers;
479 private void addToolsetControllers(List<DroolsController> droolsControllers, Object encodedClass,
480 ProtocolCoderToolset encoderSet) {
481 // figure out the right toolset
482 String groupId = encoderSet.getGroupId();
483 String artifactId = encoderSet.getArtifactId();
484 List<CoderFilters> coderFilters = encoderSet.getCoders();
485 for (CoderFilters coder : coderFilters) {
486 if (coder.getCodedClass().equals(encodedClass.getClass().getName())) {
487 var droolsController =
488 DroolsControllerConstants.getFactory().get(groupId, artifactId, "");
489 if (droolsController.ownsCoder(
490 encodedClass.getClass(), coder.getModelClassLoaderHash())) {
491 droolsControllers.add(droolsController);
498 * get all filters by maven coordinates and topic.
500 * @param groupId group id
501 * @param artifactId artifact id
503 * @return list of coders
504 * @throws IllegalArgumentException if invalid input
506 public List<CoderFilters> getFilters(String groupId, String artifactId, String topic) {
508 if (!isCodingSupported(groupId, artifactId, topic)) {
509 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
512 String key = this.codersKey(groupId, artifactId, topic);
513 ProtocolCoderToolset coderTools = coders.get(key);
514 return coderTools.getCoders();
518 * get all coders by maven coordinates and topic.
520 * @param groupId group id
521 * @param artifactId artifact id
522 * @return list of coders
523 * @throws IllegalArgumentException if invalid input
525 public List<CoderFilters> getFilters(String groupId, String artifactId) {
527 if (groupId == null || groupId.isEmpty()) {
528 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
531 if (artifactId == null || artifactId.isEmpty()) {
532 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
535 String key = this.codersKey(groupId, artifactId, "");
537 List<CoderFilters> codersFilters = new ArrayList<>();
538 for (Map.Entry<String, ProtocolCoderToolset> entry :
540 if (entry.getKey().startsWith(key)) {
541 codersFilters.addAll(entry.getValue().getCoders());
545 return codersFilters;
549 * get all filters by maven coordinates, topic, and classname.
551 * @param groupId group id
552 * @param artifactId artifact id
554 * @param classname classname
555 * @return list of coders
556 * @throws IllegalArgumentException if invalid input
558 public CoderFilters getFilters(
559 String groupId, String artifactId, String topic, String classname) {
561 if (!isCodingSupported(groupId, artifactId, topic)) {
562 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
565 if (classname == null || classname.isEmpty()) {
566 throw new IllegalArgumentException("classname must be provided");
569 String key = this.codersKey(groupId, artifactId, topic);
570 ProtocolCoderToolset coderTools = coders.get(key);
571 return coderTools.getCoder(classname);
575 * get all coders by maven coordinates and topic.
577 * @param groupId group id
578 * @param artifactId artifact id
580 * @return list of coders
581 * @throws IllegalArgumentException if invalid input
583 public ProtocolCoderToolset getCoders(
584 String groupId, String artifactId, String topic) {
586 if (!isCodingSupported(groupId, artifactId, topic)) {
587 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
590 String key = this.codersKey(groupId, artifactId, topic);
591 return coders.get(key);
595 * get all coders by maven coordinates and topic.
597 * @param groupId group id
598 * @param artifactId artifact id
599 * @return list of coders
600 * @throws IllegalArgumentException if invalid input
602 public List<ProtocolCoderToolset> getCoders(
603 String groupId, String artifactId) {
605 if (groupId == null || groupId.isEmpty()) {
606 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
609 if (artifactId == null || artifactId.isEmpty()) {
610 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
613 String key = this.codersKey(groupId, artifactId, "");
615 List<ProtocolCoderToolset> coderToolset = new ArrayList<>();
616 for (Map.Entry<String, ProtocolCoderToolset> entry :
618 if (entry.getKey().startsWith(key)) {
619 coderToolset.add(entry.getValue());
627 * get coded based on class and topic.
630 * @param codedClass class
631 * @return list of reverse filters
633 public List<CoderFilters> getReverseFilters(String topic, String codedClass) {
635 if (topic == null || topic.isEmpty()) {
636 throw new IllegalArgumentException(UNSUPPORTED_MSG);
639 if (codedClass == null) {
640 throw new IllegalArgumentException(MISSING_CLASS);
643 String key = this.reverseCodersKey(topic, codedClass);
644 List<ProtocolCoderToolset> toolsets = this.reverseCoders.get(key);
645 if (toolsets == null) {
646 throw new IllegalArgumentException("No Coder found for " + key);
649 List<CoderFilters> coderFilters = new ArrayList<>();
650 for (ProtocolCoderToolset toolset : toolsets) {
651 coderFilters.addAll(toolset.getCoders());
658 * returns group and artifact id of the creator of the encoder.
662 * @return the drools controller
664 DroolsController getDroolsController(String topic, Object fact) {
666 if (topic == null || topic.isEmpty()) {
667 throw new IllegalArgumentException(UNSUPPORTED_MSG);
671 throw new IllegalArgumentException(MISSING_CLASS);
674 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
676 if (droolsControllers.isEmpty()) {
677 throw new IllegalArgumentException("Invalid Topic: " + topic);
680 if (droolsControllers.size() > 1) {
682 "{}: multiple drools-controller {} for {}:{} ",
686 fact.getClass().getName());
689 return droolsControllers.get(0);
693 * returns group and artifact id of the creator of the encoder.
697 * @return list of drools controllers
699 List<DroolsController> getDroolsControllers(String topic, Object fact) {
701 if (topic == null || topic.isEmpty()) {
702 throw new IllegalArgumentException(UNSUPPORTED_MSG);
706 throw new IllegalArgumentException(MISSING_CLASS);
709 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
710 if (droolsControllers.size() > 1) {
713 "{}: multiple drools-controller {} for {}:{} ",
717 fact.getClass().getName());
720 return droolsControllers;
724 public String toString() {
725 return "GenericEventProtocolCoder [coders="
728 + reverseCoders.keySet()