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 lombok.ToString;
31 import org.onap.policy.drools.controller.DroolsController;
32 import org.onap.policy.drools.controller.DroolsControllerConstants;
33 import org.onap.policy.drools.protocol.coders.EventProtocolCoder.CoderFilters;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * This protocol Coder that does its best attempt to decode/encode, selecting the best class and best fitted json
42 @NoArgsConstructor(access = AccessLevel.PROTECTED)
43 abstract class GenericEventProtocolCoder {
44 private static final String INVALID_ARTIFACT_ID_MSG = "Invalid artifact id";
45 private static final String INVALID_GROUP_ID_MSG = "Invalid group id";
46 private static final String INVALID_TOPIC_MSG = "Invalid Topic";
47 private static final String UNSUPPORTED_MSG = "Unsupported";
48 private static final String UNSUPPORTED_EX_MSG = "Unsupported:";
49 private static final String MISSING_CLASS = "class must be provided";
51 private static final Logger logger = LoggerFactory.getLogger(GenericEventProtocolCoder.class);
54 * Mapping topic:controller-id -> /<protocol-decoder-toolset/> where protocol-coder-toolset contains
55 * a gson-protocol-coder-toolset.
57 protected final HashMap<String, ProtocolCoderToolset> coders =
61 * Mapping topic + classname -> Protocol Set.
63 protected final HashMap<String, List<ProtocolCoderToolset>>
64 reverseCoders = new HashMap<>();
69 public void add(EventProtocolParams eventProtocolParams) {
70 if (eventProtocolParams.getGroupId() == null || eventProtocolParams.getGroupId().isEmpty()) {
71 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
74 if (eventProtocolParams.getArtifactId() == null || eventProtocolParams.getArtifactId().isEmpty()) {
75 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
78 if (eventProtocolParams.getTopic() == null || eventProtocolParams.getTopic().isEmpty()) {
79 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
82 if (eventProtocolParams.getEventClass() == null) {
83 throw new IllegalArgumentException("Invalid Event Class");
86 String key = this.codersKey(eventProtocolParams.getGroupId(), eventProtocolParams.getArtifactId(),
87 eventProtocolParams.getTopic());
88 String reverseKey = this.reverseCodersKey(eventProtocolParams.getTopic(), eventProtocolParams.getEventClass());
91 if (coders.containsKey(key)) {
92 ProtocolCoderToolset toolset = coders.get(key);
94 logger.info("{}: adding coders for existing {}: {}", this, key, toolset);
98 eventProtocolParams.getEventClass(),
99 eventProtocolParams.getProtocolFilter(),
100 eventProtocolParams.getModelClassLoaderHash());
102 if (!reverseCoders.containsKey(reverseKey)) {
104 "{}: adding new reverse coders (multiple classes case) for {}:{}: {}",
110 List<ProtocolCoderToolset> reverseMappings =
112 reverseMappings.add(toolset);
113 reverseCoders.put(reverseKey, reverseMappings);
118 var coderTools = new GsonProtocolCoderToolset(eventProtocolParams, key);
120 logger.info("{}: adding coders for new {}: {}", this, key, coderTools);
122 coders.put(key, coderTools);
124 addReverseCoder(coderTools, key, reverseKey);
128 private void addReverseCoder(GsonProtocolCoderToolset coderTools, String key, String reverseKey) {
129 if (reverseCoders.containsKey(reverseKey)) {
130 // There is another controller (different group id/artifact id/topic)
131 // that shares the class and the topic.
133 List<ProtocolCoderToolset> toolsets =
134 reverseCoders.get(reverseKey);
136 for (ProtocolCoderToolset parserSet : toolsets) {
138 present = parserSet.getControllerId().equals(key);
142 "{}: unexpected toolset reverse mapping found for {}:{}: {}",
151 logger.info("{}: adding coder set for {}: {} ", this, reverseKey, coderTools);
152 toolsets.add(coderTools);
155 List<ProtocolCoderToolset> toolsets = new ArrayList<>();
156 toolsets.add(coderTools);
158 logger.info("{}: adding toolset for reverse key {}: {}", this, reverseKey, toolsets);
159 reverseCoders.put(reverseKey, toolsets);
164 * produces key for indexing toolset entries.
166 * @param groupId group id
167 * @param artifactId artifact id
171 protected String codersKey(String groupId, String artifactId, String topic) {
172 return groupId + ":" + artifactId + ":" + topic;
176 * produces a key for the reverse index.
179 * @param eventClass coded class
180 * @return reverse index key
182 protected String reverseCodersKey(String topic, String eventClass) {
183 return topic + ":" + eventClass;
189 * @param groupId group id
190 * @param artifactId artifact id
192 * @throws IllegalArgumentException if invalid input
194 public void remove(String groupId, String artifactId, String topic) {
196 if (groupId == null || groupId.isEmpty()) {
197 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
200 if (artifactId == null || artifactId.isEmpty()) {
201 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
204 if (topic == null || topic.isEmpty()) {
205 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
208 String key = this.codersKey(groupId, artifactId, topic);
210 synchronized (this) {
211 if (coders.containsKey(key)) {
212 ProtocolCoderToolset coderToolset = coders.remove(key);
214 logger.info("{}: removed toolset for {}: {}", this, key, coderToolset);
216 for (CoderFilters codeFilter : coderToolset.getCoders()) {
217 String className = codeFilter.getFactClass();
218 String reverseKey = this.reverseCodersKey(topic, className);
219 removeReverseCoder(key, reverseKey);
225 private void removeReverseCoder(String key, String reverseKey) {
226 if (!this.reverseCoders.containsKey(reverseKey)) {
230 List<ProtocolCoderToolset> toolsets =
231 this.reverseCoders.get(reverseKey);
232 Iterator<ProtocolCoderToolset> toolsetsIter =
234 while (toolsetsIter.hasNext()) {
235 ProtocolCoderToolset toolset = toolsetsIter.next();
236 if (toolset.getControllerId().equals(key)) {
238 "{}: removed coder from toolset for {} from reverse mapping", this, reverseKey);
239 toolsetsIter.remove();
243 if (this.reverseCoders.get(reverseKey).isEmpty()) {
244 logger.info("{}: removing reverse mapping for {}: ", this, reverseKey);
245 this.reverseCoders.remove(reverseKey);
250 * does it support coding.
252 * @param groupId group id
253 * @param artifactId artifact id
255 * @return true if its is codable
257 public boolean isCodingSupported(String groupId, String artifactId, String topic) {
259 if (groupId == null || groupId.isEmpty()) {
260 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
263 if (artifactId == null || artifactId.isEmpty()) {
264 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
267 if (topic == null || topic.isEmpty()) {
268 throw new IllegalArgumentException(INVALID_TOPIC_MSG);
271 String key = this.codersKey(groupId, artifactId, topic);
272 synchronized (this) {
273 return coders.containsKey(key);
278 * decode a json string into an Object.
280 * @param groupId group id
281 * @param artifactId artifact id
283 * @param json json string to convert to object
284 * @return the decoded object
285 * @throws IllegalArgumentException if invalid argument is provided
286 * @throws UnsupportedOperationException if the operation cannot be performed
288 public Object decode(String groupId, String artifactId, String topic, String json) {
290 if (!isCodingSupported(groupId, artifactId, topic)) {
291 throw new IllegalArgumentException(
292 UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic) + " for encoding");
295 String key = this.codersKey(groupId, artifactId, topic);
296 ProtocolCoderToolset coderTools = coders.get(key);
298 Object event = coderTools.decode(json);
302 } catch (Exception e) {
303 logger.debug("{}, cannot decode {}", this, json, e);
306 throw new UnsupportedOperationException("Cannot decode with gson");
310 * encode an object into a json string.
312 * @param groupId group id
313 * @param artifactId artifact id
315 * @param event object to convert to string
316 * @return the json string
317 * @throws IllegalArgumentException if invalid argument is provided
318 * @throws UnsupportedOperationException if the operation cannot be performed
320 public String encode(String groupId, String artifactId, String topic, Object event) {
322 if (!isCodingSupported(groupId, artifactId, topic)) {
323 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
327 throw new IllegalArgumentException("Unsupported topic:" + topic);
330 // reuse the decoder set, since there must be affinity in the model
331 String key = this.codersKey(groupId, artifactId, topic);
332 return this.encodeInternal(key, event);
336 * encode an object into a json string.
339 * @param event object to convert to string
340 * @return the json string
341 * @throws IllegalArgumentException if invalid argument is provided
342 * @throws UnsupportedOperationException if the operation cannot be performed
344 public String encode(String topic, Object event) {
347 throw new IllegalArgumentException("Invalid encoded class");
350 if (topic == null || topic.isEmpty()) {
351 throw new IllegalArgumentException("Invalid topic");
354 String reverseKey = this.reverseCodersKey(topic, event.getClass().getName());
355 if (!this.reverseCoders.containsKey(reverseKey)) {
356 throw new IllegalArgumentException("no reverse coder has been found");
359 List<ProtocolCoderToolset> toolsets =
360 this.reverseCoders.get(reverseKey);
364 toolsets.get(0).getGroupId(), toolsets.get(0).getArtifactId(), topic);
365 return this.encodeInternal(key, event);
369 * encode an object into a json string.
372 * @param encodedClass object to convert to string
373 * @return the json string
374 * @throws IllegalArgumentException if invalid argument is provided
375 * @throws UnsupportedOperationException if the operation cannot be performed
377 public String encode(String topic, Object encodedClass, DroolsController droolsController) {
379 if (encodedClass == null) {
380 throw new IllegalArgumentException("Invalid encoded class");
383 if (topic == null || topic.isEmpty()) {
384 throw new IllegalArgumentException("Invalid topic");
387 String key = codersKey(droolsController.getGroupId(), droolsController.getArtifactId(), topic);
388 return this.encodeInternal(key, encodedClass);
392 * encode an object into a json string.
394 * @param key identifier
395 * @param event object to convert to string
396 * @return the json string
397 * @throws IllegalArgumentException if invalid argument is provided
398 * @throws UnsupportedOperationException if the operation cannot be performed
400 protected String encodeInternal(String key, Object event) {
402 logger.debug("{}: encode for {}: {}", this, key, event); // NOSONAR
405 * It seems that sonar declares the previous logging line as a security vulnerability
406 * when logging the topic variable. The static code analysis indicates that
407 * the path starts in org.onap.policy.drools.server.restful.RestManager::decode(),
408 * but the request is rejected if the topic contains invalid characters (the sonar description
409 * mentions "/r/n/t" characters) all of which are validated against in the checkValidNameInput(topic).
410 * Furthermore production instances are assumed not to have debug enabled, nor the REST telemetry API
411 * should be published externally. An additional note is that Path URLs containing spaces and newlines
412 * will be rejected earlier in the HTTP protocol libraries (jetty) so an URL of the form
413 * "https://../to\npic" won't even make it here.
416 ProtocolCoderToolset coderTools = coders.get(key);
418 String json = coderTools.encode(event);
419 if (json != null && !json.isEmpty()) {
422 } catch (Exception e) {
423 logger.warn("{}: cannot encode (first) for {}: {}", this, key, event, e);
426 throw new UnsupportedOperationException("Cannot decode with gson");
433 * @param encodedClass encoded class
434 * @return list of controllers
435 * @throws IllegalStateException illegal state
436 * @throws IllegalArgumentException argument
438 protected List<DroolsController> droolsCreators(String topic, Object encodedClass) {
440 List<DroolsController> droolsControllers = new ArrayList<>();
442 String reverseKey = this.reverseCodersKey(topic, encodedClass.getClass().getName());
443 if (!this.reverseCoders.containsKey(reverseKey)) {
444 logger.warn("{}: no reverse mapping for {}", this, reverseKey);
445 return droolsControllers;
448 List<ProtocolCoderToolset> toolsets =
449 this.reverseCoders.get(reverseKey);
451 // There must be multiple toolsets associated with <topic,classname> reverseKey
452 // case 2 different controllers use the same models and register the same encoder for
453 // the same topic. This is assumed not to occur often but for the purpose of encoding
454 // but there should be no side-effects. Ownership is crosscheck against classname and
455 // classloader reference.
457 if (toolsets == null || toolsets.isEmpty()) {
458 throw new IllegalStateException(
459 "No Encoders toolsets available for topic "
462 + encodedClass.getClass().getName());
465 for (ProtocolCoderToolset encoderSet : toolsets) {
466 addToolsetControllers(droolsControllers, encodedClass, encoderSet);
469 if (droolsControllers.isEmpty()) {
470 throw new IllegalStateException(
471 "No Encoders toolsets available for "
474 + encodedClass.getClass().getName());
477 return droolsControllers;
480 private void addToolsetControllers(List<DroolsController> droolsControllers, Object encodedClass,
481 ProtocolCoderToolset encoderSet) {
482 // figure out the right toolset
483 String groupId = encoderSet.getGroupId();
484 String artifactId = encoderSet.getArtifactId();
485 List<CoderFilters> coderFilters = encoderSet.getCoders();
486 for (CoderFilters coder : coderFilters) {
487 if (coder.getFactClass().equals(encodedClass.getClass().getName())) {
488 var droolsController =
489 DroolsControllerConstants.getFactory().get(groupId, artifactId, "");
490 if (droolsController.ownsCoder(
491 encodedClass.getClass(), coder.getModelClassLoaderHash())) {
492 droolsControllers.add(droolsController);
499 * get all filters by maven coordinates and topic.
501 * @param groupId group id
502 * @param artifactId artifact id
504 * @return list of coders
505 * @throws IllegalArgumentException if invalid input
507 public List<CoderFilters> getFilters(String groupId, String artifactId, String topic) {
509 if (!isCodingSupported(groupId, artifactId, topic)) {
510 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
513 String key = this.codersKey(groupId, artifactId, topic);
514 ProtocolCoderToolset coderTools = coders.get(key);
515 return coderTools.getCoders();
519 * get all coders by maven coordinates and topic.
521 * @param groupId group id
522 * @param artifactId artifact id
523 * @return list of coders
524 * @throws IllegalArgumentException if invalid input
526 public List<CoderFilters> getFilters(String groupId, String artifactId) {
528 if (groupId == null || groupId.isEmpty()) {
529 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
532 if (artifactId == null || artifactId.isEmpty()) {
533 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
536 String key = this.codersKey(groupId, artifactId, "");
538 List<CoderFilters> codersFilters = new ArrayList<>();
539 for (Map.Entry<String, ProtocolCoderToolset> entry :
541 if (entry.getKey().startsWith(key)) {
542 codersFilters.addAll(entry.getValue().getCoders());
546 return codersFilters;
550 * get all filters by maven coordinates, topic, and classname.
552 * @param groupId group id
553 * @param artifactId artifact id
555 * @param classname classname
556 * @return list of coders
557 * @throws IllegalArgumentException if invalid input
559 public CoderFilters getFilters(
560 String groupId, String artifactId, String topic, String classname) {
562 if (!isCodingSupported(groupId, artifactId, topic)) {
563 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
566 if (classname == null || classname.isEmpty()) {
567 throw new IllegalArgumentException("classname must be provided");
570 String key = this.codersKey(groupId, artifactId, topic);
571 ProtocolCoderToolset coderTools = coders.get(key);
572 return coderTools.getCoder(classname);
576 * get all coders by maven coordinates and topic.
578 * @param groupId group id
579 * @param artifactId artifact id
581 * @return list of coders
582 * @throws IllegalArgumentException if invalid input
584 public ProtocolCoderToolset getCoders(
585 String groupId, String artifactId, String topic) {
587 if (!isCodingSupported(groupId, artifactId, topic)) {
588 throw new IllegalArgumentException(UNSUPPORTED_EX_MSG + codersKey(groupId, artifactId, topic));
591 String key = this.codersKey(groupId, artifactId, topic);
592 return coders.get(key);
596 * get all coders by maven coordinates and topic.
598 * @param groupId group id
599 * @param artifactId artifact id
600 * @return list of coders
601 * @throws IllegalArgumentException if invalid input
603 public List<ProtocolCoderToolset> getCoders(
604 String groupId, String artifactId) {
606 if (groupId == null || groupId.isEmpty()) {
607 throw new IllegalArgumentException(INVALID_GROUP_ID_MSG);
610 if (artifactId == null || artifactId.isEmpty()) {
611 throw new IllegalArgumentException(INVALID_ARTIFACT_ID_MSG);
614 String key = this.codersKey(groupId, artifactId, "");
616 List<ProtocolCoderToolset> coderToolset = new ArrayList<>();
617 for (Map.Entry<String, ProtocolCoderToolset> entry :
619 if (entry.getKey().startsWith(key)) {
620 coderToolset.add(entry.getValue());
628 * get coded based on class and topic.
631 * @param codedClass class
632 * @return list of reverse filters
634 public List<CoderFilters> getReverseFilters(String topic, String codedClass) {
636 if (topic == null || topic.isEmpty()) {
637 throw new IllegalArgumentException(UNSUPPORTED_MSG);
640 if (codedClass == null) {
641 throw new IllegalArgumentException(MISSING_CLASS);
644 String key = this.reverseCodersKey(topic, codedClass);
645 List<ProtocolCoderToolset> toolsets = this.reverseCoders.get(key);
646 if (toolsets == null) {
647 throw new IllegalArgumentException("No Coder found for " + key);
650 List<CoderFilters> coderFilters = new ArrayList<>();
651 for (ProtocolCoderToolset toolset : toolsets) {
652 coderFilters.addAll(toolset.getCoders());
659 * returns group and artifact id of the creator of the encoder.
663 * @return the drools controller
665 DroolsController getDroolsController(String topic, Object fact) {
667 if (topic == null || topic.isEmpty()) {
668 throw new IllegalArgumentException(UNSUPPORTED_MSG);
672 throw new IllegalArgumentException(MISSING_CLASS);
675 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
677 if (droolsControllers.isEmpty()) {
678 throw new IllegalArgumentException("Invalid Topic: " + topic);
681 if (droolsControllers.size() > 1) {
683 "{}: multiple drools-controller {} for {}:{} ",
687 fact.getClass().getName());
690 return droolsControllers.get(0);
694 * returns group and artifact id of the creator of the encoder.
698 * @return list of drools controllers
700 List<DroolsController> getDroolsControllers(String topic, Object fact) {
702 if (topic == null || topic.isEmpty()) {
703 throw new IllegalArgumentException(UNSUPPORTED_MSG);
707 throw new IllegalArgumentException(MISSING_CLASS);
710 List<DroolsController> droolsControllers = droolsCreators(topic, fact);
711 if (droolsControllers.size() > 1) {
714 "{}: multiple drools-controller {} for {}:{} ",
718 fact.getClass().getName());
721 return droolsControllers;