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