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;