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 lombok.AccessLevel;
29 import lombok.NoArgsConstructor;
30 import org.onap.policy.drools.controller.DroolsController;
31 import org.onap.policy.drools.controller.DroolsControllerConstants;
32 import org.onap.policy.drools.protocol.coders.EventProtocolCoder.CoderFilters;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * This protocol Coder that does its best attempt to decode/encode, selecting the best class and best fitted json
40 @NoArgsConstructor(access = AccessLevel.PROTECTED)
41 abstract class GenericEventProtocolCoder {
42 private static final String INVALID_ARTIFACT_ID_MSG = "Invalid artifact id";
43 private static final String INVALID_GROUP_ID_MSG = "Invalid group id";
44 private static final String INVALID_TOPIC_MSG = "Invalid Topic";
45 private static final String UNSUPPORTED_MSG = "Unsupported";
46 private static final String UNSUPPORTED_EX_MSG = "Unsupported:";
47 private static final String MISSING_CLASS = "class must be provided";
49 private static final Logger logger = LoggerFactory.getLogger(GenericEventProtocolCoder.class);
52 * Mapping topic:controller-id -> /<protocol-decoder-toolset/> where protocol-coder-toolset contains
53 * a gson-protocol-coder-toolset.
55 protected final HashMap<String, ProtocolCoderToolset> coders =
59 * Mapping topic + classname -> Protocol Set.
61 protected final HashMap<String, List<ProtocolCoderToolset>>
62 reverseCoders = new HashMap<>();
67 public void add(EventProtocolParams eventProtocolParams) {
68 if (eventProtocolParams.getGroupId() == null || eventProtocolParams.getGroupId().isEmpty()) {
69 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
72 if (eventProtocolParams.getArtifactId() == null || eventProtocolParams.getArtifactId().isEmpty()) {
73 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
76 if (eventProtocolParams.getTopic() == null || eventProtocolParams.getTopic().isEmpty()) {
77 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
80 if (eventProtocolParams.getEventClass() == null) {
81 throw new IllegalArgumentException("Invalid Event Class");
84 String key = this.codersKey(eventProtocolParams.getGroupId(), eventProtocolParams.getArtifactId(),
85 eventProtocolParams.getTopic());
86 String reverseKey = this.reverseCodersKey(eventProtocolParams.getTopic(), eventProtocolParams.getEventClass());
89 if (coders.containsKey(key)) {
90 ProtocolCoderToolset toolset = coders.get(key);
92 logger.info("{}: adding coders for existing {}: {}", this, key, toolset);
96 eventProtocolParams.getEventClass(),
97 eventProtocolParams.getProtocolFilter(),
98 eventProtocolParams.getModelClassLoaderHash());
100 if (!reverseCoders.containsKey(reverseKey)) {
102 "{}: adding new reverse coders (multiple classes case) for {}:{}: {}",
108 List<ProtocolCoderToolset> reverseMappings =
110 reverseMappings.add(toolset);
111 reverseCoders.put(reverseKey, reverseMappings);
116 var coderTools = new GsonProtocolCoderToolset(eventProtocolParams, key);
118 logger.info("{}: adding coders for new {}: {}", this, key, coderTools);
120 coders.put(key, coderTools);
122 addReverseCoder(coderTools, key, reverseKey);
126 private void addReverseCoder(GsonProtocolCoderToolset coderTools, String key, String reverseKey) {
127 if (reverseCoders.containsKey(reverseKey)) {
128 // There is another controller (different group id/artifact id/topic)
129 // that shares the class and the topic.
131 List<ProtocolCoderToolset> toolsets =
132 reverseCoders.get(reverseKey);
134 for (ProtocolCoderToolset parserSet : toolsets) {
136 present = parserSet.getControllerId().equals(key);
140 "{}: unexpected toolset reverse mapping found for {}:{}: {}",
149 logger.info("{}: adding coder set for {}: {} ", this, reverseKey, coderTools);
150 toolsets.add(coderTools);
153 List<ProtocolCoderToolset> toolsets = new ArrayList<>();
154 toolsets.add(coderTools);
156 logger.info("{}: adding toolset for reverse key {}: {}", this, reverseKey, toolsets);
157 reverseCoders.put(reverseKey, toolsets);
162 * produces key for indexing toolset entries.
164 * @param groupId group id
165 * @param artifactId artifact id
169 protected String codersKey(String groupId, String artifactId, String topic) {
170 return groupId + ":" + artifactId + ":" + topic;
174 * produces a key for the reverse index.
177 * @param eventClass coded class
178 * @return reverse index key
180 protected String reverseCodersKey(String topic, String eventClass) {
181 return topic + ":" + eventClass;
187 * @param groupId group id
188 * @param artifactId artifact id
190 * @throws IllegalArgumentException if invalid input
192 public void remove(String groupId, String artifactId, String topic) {
194 if (groupId == null || groupId.isEmpty()) {
195 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
198 if (artifactId == null || artifactId.isEmpty()) {
199 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
202 if (topic == null || topic.isEmpty()) {
203 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
206 String key = this.codersKey(groupId, artifactId, topic);
208 synchronized (this) {
209 if (coders.containsKey(key)) {
210 ProtocolCoderToolset coderToolset = coders.remove(key);
212 logger.info("{}: removed toolset for {}: {}", this, key, coderToolset);
214 for (CoderFilters codeFilter : coderToolset.getCoders()) {
215 String className = codeFilter.getFactClass();
216 String reverseKey = this.reverseCodersKey(topic, className);
217 removeReverseCoder(key, reverseKey);
223 private void removeReverseCoder(String key, String reverseKey) {
224 if (!this.reverseCoders.containsKey(reverseKey)) {
228 List<ProtocolCoderToolset> toolsets =
229 this.reverseCoders.get(reverseKey);
230 Iterator<ProtocolCoderToolset> toolsetsIter =
232 while (toolsetsIter.hasNext()) {
233 ProtocolCoderToolset toolset = toolsetsIter.next();
234 if (toolset.getControllerId().equals(key)) {
236 "{}: removed coder from toolset for {} from reverse mapping", this, reverseKey);
237 toolsetsIter.remove();
241 if (this.reverseCoders.get(reverseKey).isEmpty()) {
242 logger.info("{}: removing reverse mapping for {}: ", this, reverseKey);
243 this.reverseCoders.remove(reverseKey);
248 * does it support coding.
250 * @param groupId group id
251 * @param artifactId artifact id
253 * @return true if its is codable
255 public boolean isCodingSupported(String groupId, String artifactId, String topic) {
257 if (groupId == null || groupId.isEmpty()) {
258 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
261 if (artifactId == null || artifactId.isEmpty()) {
262 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
265 if (topic == null || topic.isEmpty()) {
266 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
269 String key = this.codersKey(groupId, artifactId, topic);
270 synchronized (this) {
271 return coders.containsKey(key);
276 * decode a json string into an Object.
278 * @param groupId group id
279 * @param artifactId artifact id
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
286 public Object decode(String groupId, String artifactId, String topic, String json) {
288 if (!isCodingSupported(groupId, artifactId, topic)) {
289 throw new IllegalArgumentException(
290 UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic) + " for encoding");
293 String key = this.codersKey(groupId, artifactId, topic);
294 ProtocolCoderToolset coderTools = coders.get(key);
296 Object event = coderTools.decode(json);
300 } catch (Exception e) {
301 logger.debug("{}, cannot decode {}", this, json, e);
304 throw new UnsupportedOperationException("Cannot decode with gson");
308 * encode an object into a json string.
310 * @param groupId group id
311 * @param artifactId artifact id
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
318 public String encode(String groupId, String artifactId, String topic, Object event) {
320 if (!isCodingSupported(groupId, artifactId, topic)) {
321 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
325 throw new IllegalArgumentException("Unsupported topic:" + topic);
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);
334 * encode an object into a json string.
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
342 public String encode(String topic, Object event) {
345 throw new IllegalArgumentException("Invalid encoded class");
348 if (topic == null || topic.isEmpty()) {
349 throw new IllegalArgumentException("Invalid topic");
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");
357 List<ProtocolCoderToolset> toolsets =
358 this.reverseCoders.get(reverseKey);
362 toolsets.get(0).getGroupId(), toolsets.get(0).getArtifactId(), topic);
363 return this.encodeInternal(key, event);
367 * encode an object into a json string.
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
375 public String encode(String topic, Object encodedClass, DroolsController droolsController) {
377 if (encodedClass == null) {
378 throw new IllegalArgumentException("Invalid encoded class");
381 if (topic == null || topic.isEmpty()) {
382 throw new IllegalArgumentException("Invalid topic");
385 String key = codersKey(droolsController.getGroupId(), droolsController.getArtifactId(), topic);
386 return this.encodeInternal(key, encodedClass);
390 * encode an object into a json string.
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
398 protected String encodeInternal(String key, Object event) {
400 logger.debug("{}: encode for {}: {}", this, key, event); // NOSONAR
403 * It seems that sonar declares the previous logging line as a security vulnerability
404 * when logging the topic variable. The static code analysis indicates that
405 * the path starts in org.onap.policy.drools.server.restful.RestManager::decode(),
406 * but the request is rejected if the topic contains invalid characters (the sonar description
407 * mentions "/r/n/t" characters) all of which are validated against in the checkValidNameInput(topic).
408 * Furthermore production instances are assumed not to have debug enabled, nor the REST telemetry API
409 * should be published externally. An additional note is that Path URLs containing spaces and newlines
410 * will be rejected earlier in the HTTP protocol libraries (jetty) so an URL of the form
411 * "https://../to\npic" won't even make it here.
414 ProtocolCoderToolset coderTools = coders.get(key);
416 String json = coderTools.encode(event);
417 if (json != null && !json.isEmpty()) {
420 } catch (Exception e) {
421 logger.warn("{}: cannot encode (first) for {}: {}", this, key, event, e);
424 throw new UnsupportedOperationException("Cannot decode with gson");
431 * @param encodedClass encoded class
432 * @return list of controllers
433 * @throws IllegalStateException illegal state
434 * @throws IllegalArgumentException argument
436 protected List<DroolsController> droolsCreators(String topic, Object encodedClass) {
438 List<DroolsController> droolsControllers = new ArrayList<>();
440 String reverseKey = this.reverseCodersKey(topic, encodedClass.getClass().getName());
441 if (!this.reverseCoders.containsKey(reverseKey)) {
442 logger.warn("{}: no reverse mapping for {}", this, reverseKey);
443 return droolsControllers;
446 List<ProtocolCoderToolset> toolsets =
447 this.reverseCoders.get(reverseKey);
449 // There must be multiple toolsets associated with <topic,classname> reverseKey
450 // case 2 different controllers use the same models and register the same encoder for
451 // the same topic. This is assumed not to occur often but for the purpose of encoding
452 // but there should be no side-effects. Ownership is crosscheck against classname and
453 // classloader reference.
455 if (toolsets == null || toolsets.isEmpty()) {
456 throw new IllegalStateException(
457 "No Encoders toolsets available for topic "
460 + encodedClass.getClass().getName());
463 for (ProtocolCoderToolset encoderSet : toolsets) {
464 addToolsetControllers(droolsControllers, encodedClass, encoderSet);
467 if (droolsControllers.isEmpty()) {
468 throw new IllegalStateException(
469 "No Encoders toolsets available for "
472 + encodedClass.getClass().getName());
475 return droolsControllers;
478 private void addToolsetControllers(List<DroolsController> droolsControllers, Object encodedClass,
479 ProtocolCoderToolset encoderSet) {
480 // figure out the right toolset
481 String groupId = encoderSet.getGroupId();
482 String artifactId = encoderSet.getArtifactId();
483 List<CoderFilters> coderFilters = encoderSet.getCoders();
484 for (CoderFilters coder : coderFilters) {
485 if (coder.getFactClass().equals(encodedClass.getClass().getName())) {
486 var droolsController =
487 DroolsControllerConstants.getFactory().get(groupId, artifactId, "");
488 if (droolsController.ownsCoder(
489 encodedClass.getClass(), coder.getModelClassLoaderHash())) {
490 droolsControllers.add(droolsController);
497 * get all filters by maven coordinates and topic.
499 * @param groupId group id
500 * @param artifactId artifact id
502 * @return list of coders
503 * @throws IllegalArgumentException if invalid input
505 public List<CoderFilters> getFilters(String groupId, String artifactId, String topic) {
507 if (!isCodingSupported(groupId, artifactId, topic)) {
508 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
511 String key = this.codersKey(groupId, artifactId, topic);
512 ProtocolCoderToolset coderTools = coders.get(key);
513 return coderTools.getCoders();
517 * get all coders by maven coordinates and topic.
519 * @param groupId group id
520 * @param artifactId artifact id
521 * @return list of coders
522 * @throws IllegalArgumentException if invalid input
524 public List<CoderFilters> getFilters(String groupId, String artifactId) {
526 if (groupId == null || groupId.isEmpty()) {
527 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
530 if (artifactId == null || artifactId.isEmpty()) {
531 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
534 String key = this.codersKey(groupId, artifactId, "");
536 List<CoderFilters> codersFilters = new ArrayList<>();
537 for (Map.Entry<String, ProtocolCoderToolset> entry :
539 if (entry.getKey().startsWith(key)) {
540 codersFilters.addAll(entry.getValue().getCoders());
544 return codersFilters;
548 * get all filters by maven coordinates, topic, and classname.
550 * @param groupId group id
551 * @param artifactId artifact id
553 * @param classname classname
554 * @return list of coders
555 * @throws IllegalArgumentException if invalid input
557 public CoderFilters getFilters(
558 String groupId, String artifactId, String topic, String classname) {
560 if (!isCodingSupported(groupId, artifactId, topic)) {
561 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
564 if (classname == null || classname.isEmpty()) {
565 throw new IllegalArgumentException("classname must be provided");
568 String key = this.codersKey(groupId, artifactId, topic);
569 ProtocolCoderToolset coderTools = coders.get(key);
570 return coderTools.getCoder(classname);
574 * get all coders by maven coordinates and topic.
576 * @param groupId group id
577 * @param artifactId artifact id
579 * @return list of coders
580 * @throws IllegalArgumentException if invalid input
582 public ProtocolCoderToolset getCoders(
583 String groupId, String artifactId, String topic) {
585 if (!isCodingSupported(groupId, artifactId, topic)) {
586 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
589 String key = this.codersKey(groupId, artifactId, topic);
590 return coders.get(key);
594 * get all coders by maven coordinates and topic.
596 * @param groupId group id
597 * @param artifactId artifact id
598 * @return list of coders
599 * @throws IllegalArgumentException if invalid input
601 public List<ProtocolCoderToolset> getCoders(
602 String groupId, String artifactId) {
604 if (groupId == null || groupId.isEmpty()) {
605 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
608 if (artifactId == null || artifactId.isEmpty()) {
609 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
612 String key = this.codersKey(groupId, artifactId, "");
614 List<ProtocolCoderToolset> coderToolset = new ArrayList<>();
615 for (Map.Entry<String, ProtocolCoderToolset> entry :
617 if (entry.getKey().startsWith(key)) {
618 coderToolset.add(entry.getValue());
626 * get coded based on class and topic.
629 * @param codedClass class
630 * @return list of reverse filters
632 public List<CoderFilters> getReverseFilters(String topic, String codedClass) {
634 if (topic == null || topic.isEmpty()) {
635 throw new IllegalArgumentException(UNSUPPORTED_MSG);
638 if (codedClass == null) {
639 throw new IllegalArgumentException(MISSING_CLASS);
642 String key = this.reverseCodersKey(topic, codedClass);
643 List<ProtocolCoderToolset> toolsets = this.reverseCoders.get(key);
644 if (toolsets == null) {
645 throw new IllegalArgumentException("No Coder found for " + key);
648 List<CoderFilters> coderFilters = new ArrayList<>();
649 for (ProtocolCoderToolset toolset : toolsets) {
650 coderFilters.addAll(toolset.getCoders());
657 * returns group and artifact id of the creator of the encoder.
661 * @return the drools controller
663 DroolsController getDroolsController(String topic, Object fact) {
665 if (topic == null || topic.isEmpty()) {
666 throw new IllegalArgumentException(UNSUPPORTED_MSG);
670 throw new IllegalArgumentException(MISSING_CLASS);
673 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
675 if (droolsControllers.isEmpty()) {
676 throw new IllegalArgumentException("Invalid Topic: " + topic);
679 if (droolsControllers.size() > 1) {
681 "{}: multiple drools-controller {} for {}:{} ",
685 fact.getClass().getName());
688 return droolsControllers.get(0);
692 * returns group and artifact id of the creator of the encoder.
696 * @return list of drools controllers
698 List<DroolsController> getDroolsControllers(String topic, Object fact) {
700 if (topic == null || topic.isEmpty()) {
701 throw new IllegalArgumentException(UNSUPPORTED_MSG);
705 throw new IllegalArgumentException(MISSING_CLASS);
708 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
709 if (droolsControllers.size() > 1) {
712 "{}: multiple drools-controller {} for {}:{} ",
716 fact.getClass().getName());
719 return droolsControllers;
723 * Note: this only logs the KEYSETS, thus lombok ToString annotation is not used.
724 * Otherwise, it results in too much verbosity.
727 public String toString() {
728 return "GenericEventProtocolCoder [coders="
731 + reverseCoders.keySet()