More sonars in drools-pdp
[policy/drools-pdp.git] / policy-management / src / main / java / org / onap / policy / drools / controller / internal / MavenDroolsController.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2017-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.controller.internal;
22
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.stream.Collectors;
30 import org.apache.commons.collections4.queue.CircularFifoQueue;
31 import org.checkerframework.checker.nullness.qual.NonNull;
32 import org.drools.core.ClassObjectFilter;
33 import org.kie.api.definition.KiePackage;
34 import org.kie.api.definition.rule.Query;
35 import org.kie.api.runtime.KieSession;
36 import org.kie.api.runtime.rule.FactHandle;
37 import org.kie.api.runtime.rule.QueryResults;
38 import org.kie.api.runtime.rule.QueryResultsRow;
39 import org.onap.policy.common.endpoints.event.comm.TopicSink;
40 import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
41 import org.onap.policy.common.gson.annotation.GsonJsonProperty;
42 import org.onap.policy.common.utils.services.FeatureApiUtils;
43 import org.onap.policy.common.utils.services.OrderedServiceImpl;
44 import org.onap.policy.drools.controller.DroolsController;
45 import org.onap.policy.drools.controller.DroolsControllerConstants;
46 import org.onap.policy.drools.core.PolicyContainer;
47 import org.onap.policy.drools.core.PolicySession;
48 import org.onap.policy.drools.core.jmx.PdpJmx;
49 import org.onap.policy.drools.features.DroolsControllerFeatureApi;
50 import org.onap.policy.drools.features.DroolsControllerFeatureApiConstants;
51 import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
52 import org.onap.policy.drools.protocol.coders.EventProtocolCoderConstants;
53 import org.onap.policy.drools.protocol.coders.EventProtocolParams;
54 import org.onap.policy.drools.protocol.coders.JsonProtocolFilter;
55 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration;
56 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.CustomGsonCoder;
57 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.PotentialCoderFilter;
58 import org.onap.policy.drools.utils.ReflectionUtil;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * Maven-based Drools Controller that interacts with the
64  * policy-core PolicyContainer and PolicySession to manage
65  * Drools containers instantiated using Maven.
66  */
67 public class MavenDroolsController implements DroolsController {
68
69     private static final String FACT_RETRIEVE_ERROR = "Object cannot be retrieved from fact {}";
70
71     /**
72      * logger.
73      */
74     private static Logger  logger = LoggerFactory.getLogger(MavenDroolsController.class);
75
76     /**
77      * Policy Container, the access object to the policy-core layer.
78      */
79     @GsonJsonIgnore
80     protected final PolicyContainer policyContainer;
81
82     /**
83      * alive status of this drools controller,
84      * reflects invocation of start()/stop() only.
85      */
86     protected volatile boolean alive = false;
87
88     /**
89      * locked status of this drools controller,
90      * reflects if i/o drools related operations are permitted,
91      * more specifically: offer() and deliver().
92      * It does not affect the ability to start and stop
93      * underlying drools infrastructure
94      */
95     protected volatile boolean locked = false;
96
97     /**
98      * list of topics, each with associated decoder classes, each
99      * with a list of associated filters.
100      */
101     protected List<TopicCoderFilterConfiguration> decoderConfigurations;
102
103     /**
104      * list of topics, each with associated encoder classes, each
105      * with a list of associated filters.
106      */
107     protected List<TopicCoderFilterConfiguration> encoderConfigurations;
108
109     /**
110      * recent source events processed.
111      */
112     protected final CircularFifoQueue<Object> recentSourceEvents = new CircularFifoQueue<>(10);
113
114     /**
115      * recent sink events processed.
116      */
117     protected final CircularFifoQueue<String> recentSinkEvents = new CircularFifoQueue<>(10);
118
119     /**
120      * original Drools Model/Rules classloader hash.
121      */
122     protected int modelClassLoaderHash;
123
124     /**
125      * Expanded version of the constructor.
126      *
127      * @param groupId maven group id
128      * @param artifactId maven artifact id
129      * @param version maven version
130      * @param decoderConfigurations list of topic -> decoders -> filters mapping
131      * @param encoderConfigurations list of topic -> encoders -> filters mapping
132      *
133      * @throws IllegalArgumentException invalid arguments passed in
134      */
135     public MavenDroolsController(String groupId,
136             String artifactId,
137             String version,
138             List<TopicCoderFilterConfiguration> decoderConfigurations,
139             List<TopicCoderFilterConfiguration> encoderConfigurations) {
140
141         logger.info("drools-controller instantiation [{}:{}:{}]", groupId, artifactId, version);
142
143         if (groupId == null || groupId.isEmpty()) {
144             throw new IllegalArgumentException("Missing maven group-id coordinate");
145         }
146
147         if (artifactId == null || artifactId.isEmpty()) {
148             throw new IllegalArgumentException("Missing maven artifact-id coordinate");
149         }
150
151         if (version == null || version.isEmpty()) {
152             throw new IllegalArgumentException("Missing maven version coordinate");
153         }
154
155         this.policyContainer = makePolicyContainer(groupId, artifactId, version);
156         this.init(decoderConfigurations, encoderConfigurations);
157
158         logger.debug("{}: instantiation completed ", this);
159     }
160
161     /**
162      * init encoding/decoding configuration.
163      *
164      * @param decoderConfigurations list of topic -> decoders -> filters mapping
165      * @param encoderConfigurations list of topic -> encoders -> filters mapping
166      */
167     protected void init(List<TopicCoderFilterConfiguration> decoderConfigurations,
168             List<TopicCoderFilterConfiguration> encoderConfigurations) {
169
170         this.decoderConfigurations = decoderConfigurations;
171         this.encoderConfigurations = encoderConfigurations;
172
173         this.initCoders(decoderConfigurations, true);
174         this.initCoders(encoderConfigurations, false);
175
176         this.modelClassLoaderHash = this.policyContainer.getClassLoader().hashCode();
177     }
178
179     @Override
180     public void updateToVersion(String newGroupId, String newArtifactId, String newVersion,
181             List<TopicCoderFilterConfiguration> decoderConfigurations,
182             List<TopicCoderFilterConfiguration> encoderConfigurations)
183                     throws LinkageError {
184
185         logger.info("updating version -> [{}:{}:{}]", newGroupId, newArtifactId, newVersion);
186
187         validateText(newGroupId, "Missing maven group-id coordinate");
188         validateText(newArtifactId, "Missing maven artifact-id coordinate");
189         validateText(newVersion, "Missing maven version coordinate");
190
191         validateHasBrain(newGroupId, newArtifactId, newVersion);
192
193         if (newGroupId.equalsIgnoreCase(this.getGroupId())
194                 && newArtifactId.equalsIgnoreCase(this.getArtifactId())
195                         && newVersion.equalsIgnoreCase(this.getVersion())) {
196             logger.warn("All in the right version: {}:{}:{} vs. {}", newGroupId, newArtifactId, newVersion, this);
197             return;
198         }
199
200         validateNewVersion(newGroupId, newArtifactId, newVersion);
201
202         /* upgrade */
203         String messages = this.policyContainer.updateToVersion(newVersion);
204         logger.warn("{} UPGRADE results: {}", this, messages);
205
206         /*
207          * If all sucessful (can load new container), now we can remove all coders from previous sessions
208          */
209         this.removeCoders();
210
211         /*
212          * add the new coders
213          */
214         this.init(decoderConfigurations, encoderConfigurations);
215
216         logger.info("UPDATE-TO-VERSION: completed {}", this);
217     }
218
219     private void validateText(String text, String errorMessage) {
220         if (text == null || text.isEmpty()) {
221             throw new IllegalArgumentException(errorMessage);
222         }
223     }
224
225     private void validateHasBrain(String newGroupId, String newArtifactId, String newVersion) {
226         if (newGroupId.equalsIgnoreCase(DroolsControllerConstants.NO_GROUP_ID)
227                 || newArtifactId.equalsIgnoreCase(DroolsControllerConstants.NO_ARTIFACT_ID)
228                 || newVersion.equalsIgnoreCase(DroolsControllerConstants.NO_VERSION)) {
229             throw new IllegalArgumentException("BRAINLESS maven coordinates provided: "
230                     + newGroupId + ":" + newArtifactId + ":"
231                     + newVersion);
232         }
233     }
234
235     private void validateNewVersion(String newGroupId, String newArtifactId, String newVersion) {
236         if (!newGroupId.equalsIgnoreCase(this.getGroupId())
237                 || !newArtifactId.equalsIgnoreCase(this.getArtifactId())) {
238             throw new IllegalArgumentException(
239                     "Group ID and Artifact ID maven coordinates must be identical for the upgrade: "
240                     + newGroupId + ":" + newArtifactId + ":"
241                     + newVersion + " vs. " + this);
242         }
243     }
244
245     /**
246      * initialize decoders for all the topics supported by this controller
247      * Note this is critical to be done after the Policy Container is
248      * instantiated to be able to fetch the corresponding classes.
249      *
250      * @param coderConfigurations list of topic -> decoders -> filters mapping
251      */
252     protected void initCoders(List<TopicCoderFilterConfiguration> coderConfigurations,
253             boolean decoder) {
254
255         logger.info("INIT-CODERS: {}", this);
256
257         if (coderConfigurations == null) {
258             return;
259         }
260
261
262         for (TopicCoderFilterConfiguration coderConfig: coderConfigurations) {
263             String topic = coderConfig.getTopic();
264
265             CustomGsonCoder customGsonCoder = getCustomCoder(coderConfig);
266
267             List<PotentialCoderFilter> coderFilters = coderConfig.getCoderFilters();
268             if (coderFilters == null || coderFilters.isEmpty()) {
269                 continue;
270             }
271
272             for (PotentialCoderFilter coderFilter : coderFilters) {
273                 String potentialCodedClass = coderFilter.getCodedClass();
274                 JsonProtocolFilter protocolFilter = coderFilter.getFilter();
275
276                 if (!isClass(potentialCodedClass)) {
277                     throw makeRetrieveEx(potentialCodedClass);
278                 } else {
279                     logClassFetched(potentialCodedClass);
280                 }
281
282                 if (decoder) {
283                     getCoderManager().addDecoder(EventProtocolParams.builder()
284                             .groupId(this.getGroupId())
285                             .artifactId(this.getArtifactId())
286                             .topic(topic)
287                             .eventClass(potentialCodedClass)
288                             .protocolFilter(protocolFilter)
289                             .customGsonCoder(customGsonCoder)
290                             .modelClassLoaderHash(this.policyContainer.getClassLoader().hashCode()));
291                 } else {
292                     getCoderManager().addEncoder(
293                             EventProtocolParams.builder().groupId(this.getGroupId())
294                                     .artifactId(this.getArtifactId()).topic(topic)
295                                     .eventClass(potentialCodedClass).protocolFilter(protocolFilter)
296                                     .customGsonCoder(customGsonCoder)
297                                     .modelClassLoaderHash(this.policyContainer.getClassLoader().hashCode()));
298                 }
299             }
300         }
301     }
302
303     private CustomGsonCoder getCustomCoder(TopicCoderFilterConfiguration coderConfig) {
304         CustomGsonCoder customGsonCoder = coderConfig.getCustomGsonCoder();
305         if (customGsonCoder != null
306                 && customGsonCoder.getClassContainer() != null
307                 && !customGsonCoder.getClassContainer().isEmpty()) {
308
309             String customGsonCoderClass = customGsonCoder.getClassContainer();
310             if (!isClass(customGsonCoderClass)) {
311                 throw makeRetrieveEx(customGsonCoderClass);
312             } else {
313                 logClassFetched(customGsonCoderClass);
314             }
315         }
316         return customGsonCoder;
317     }
318
319     /**
320      * Logs an error and makes an exception for an item that cannot be retrieved.
321      * @param itemName the item to retrieve
322      * @return a new exception
323      */
324     private IllegalArgumentException makeRetrieveEx(String itemName) {
325         logger.error("{} cannot be retrieved", itemName);
326         return new IllegalArgumentException(itemName + " cannot be retrieved");
327     }
328
329     /**
330      * Logs the name of the class that was fetched.
331      * @param className class name fetched
332      */
333     private void logClassFetched(String className) {
334         logger.info("CLASS FETCHED {}", className);
335     }
336
337
338     /**
339      * remove decoders.
340      */
341     protected void removeDecoders() {
342         logger.info("REMOVE-DECODERS: {}", this);
343
344         if (this.decoderConfigurations == null) {
345             return;
346         }
347
348
349         for (TopicCoderFilterConfiguration coderConfig: decoderConfigurations) {
350             String topic = coderConfig.getTopic();
351             getCoderManager().removeDecoders(this.getGroupId(), this.getArtifactId(), topic);
352         }
353     }
354
355     /**
356      * remove decoders.
357      */
358     protected void removeEncoders() {
359
360         logger.info("REMOVE-ENCODERS: {}", this);
361
362         if (this.encoderConfigurations == null) {
363             return;
364         }
365
366         for (TopicCoderFilterConfiguration coderConfig: encoderConfigurations) {
367             String topic = coderConfig.getTopic();
368             getCoderManager().removeEncoders(this.getGroupId(), this.getArtifactId(), topic);
369         }
370     }
371
372
373     @Override
374     public boolean ownsCoder(Class<?> coderClass, int modelHash) {
375         if (!isClass(coderClass.getName())) {
376             logger.error("{}{} cannot be retrieved. ", this, coderClass.getName());
377             return false;
378         }
379
380         if (modelHash == this.modelClassLoaderHash) {
381             logger.info("{}{} class loader matches original drools controller rules classloader {}",
382                             coderClass.getName(), this, coderClass.getClassLoader());
383             return true;
384         } else {
385             logger.warn("{}{} class loaders don't match {} vs {}", this, coderClass.getName(),
386                             coderClass.getClassLoader(), this.policyContainer.getClassLoader());
387             return false;
388         }
389     }
390
391     @Override
392     public boolean start() {
393
394         logger.info("START: {}", this);
395
396         synchronized (this) {
397             if (this.alive) {
398                 return true;
399             }
400             this.alive = true;
401         }
402
403         return this.policyContainer.start();
404     }
405
406     @Override
407     public boolean stop() {
408
409         logger.info("STOP: {}", this);
410
411         synchronized (this) {
412             if (!this.alive) {
413                 return true;
414             }
415             this.alive = false;
416         }
417
418         return this.policyContainer.stop();
419     }
420
421     @Override
422     public void shutdown() {
423         logger.info("{}: SHUTDOWN", this);
424
425         try {
426             this.stop();
427             this.removeCoders();
428         } catch (Exception e) {
429             logger.error("{} SHUTDOWN FAILED because of {}", this, e.getMessage(), e);
430         } finally {
431             this.policyContainer.shutdown();
432         }
433
434     }
435
436     @Override
437     public void halt() {
438         logger.info("{}: HALT", this);
439
440         try {
441             this.stop();
442             this.removeCoders();
443         } catch (Exception e) {
444             logger.error("{} HALT FAILED because of {}", this, e.getMessage(), e);
445         } finally {
446             this.policyContainer.destroy();
447         }
448     }
449
450     /**
451      * removes this drools controllers and encoders and decoders from operation.
452      */
453     protected void removeCoders() {
454         logger.info("{}: REMOVE-CODERS", this);
455
456         try {
457             this.removeDecoders();
458         } catch (IllegalArgumentException e) {
459             logger.error("{} REMOVE-DECODERS FAILED because of {}", this, e.getMessage(), e);
460         }
461
462         try {
463             this.removeEncoders();
464         } catch (IllegalArgumentException e) {
465             logger.error("{} REMOVE-ENCODERS FAILED because of {}", this, e.getMessage(), e);
466         }
467     }
468
469     @Override
470     public boolean isAlive() {
471         return this.alive;
472     }
473
474     @Override
475     public boolean offer(String topic, String event) {
476         logger.debug("{}: OFFER raw event from {}", this, topic);
477
478         if (this.locked || !this.alive || this.policyContainer.getPolicySessions().isEmpty()) {
479             return true;
480         }
481
482         // 1. Now, check if this topic has a decoder:
483
484         if (!getCoderManager().isDecodingSupported(this.getGroupId(),
485                 this.getArtifactId(),
486                 topic)) {
487
488             logger.warn("{}: DECODING-UNSUPPORTED {}:{}:{}", this,              // NOSONAR
489                     topic, this.getGroupId(), this.getArtifactId());
490             return true;
491         }
492
493         // 2. Decode
494
495         Object anEvent;
496         try {
497             anEvent = getCoderManager().decode(this.getGroupId(),
498                     this.getArtifactId(),
499                     topic,
500                     event);
501         } catch (UnsupportedOperationException uoe) {
502             logger.debug("{}: DECODE FAILED: {} <- {} because of {}", this, topic,
503                     event, uoe.getMessage(), uoe);
504             return true;
505         } catch (Exception e) {
506             logger.warn("{}: DECODE FAILED: {} <- {} because of {}", this, topic,
507                     event, e.getMessage(), e);
508             return true;
509         }
510
511         return offer(anEvent);
512
513     }
514
515     /*
516      * This method always returns "true", which causes a sonar complaint. However,
517      * refactoring or restructuring it would unnecessarily complicate it, thus we'll just
518      * disable the sonar complaint.
519      */
520     @Override
521     public <T> boolean offer(T event) {     // NOSONAR
522         logger.debug("{}: OFFER event", this);
523
524         if (this.locked || !this.alive || this.policyContainer.getPolicySessions().isEmpty()) {
525             return true;
526         }
527
528         synchronized (this.recentSourceEvents) {
529             this.recentSourceEvents.add(event);
530         }
531
532         PdpJmx.getInstance().updateOccured();
533
534         // Broadcast
535
536         if (FeatureApiUtils.apply(getDroolsProviders().getList(),
537             feature -> feature.beforeInsert(this, event),
538             (feature, ex) -> logger.error("{}: feature {} before-insert failure because of {}", this,
539                             feature.getClass().getName(), ex.getMessage(), ex))) {
540             return true;
541         }
542
543         boolean successInject = this.policyContainer.insertAll(event);
544         if (!successInject) {
545             logger.warn("{} Failed to inject into PolicyContainer {}", this, this.getSessionNames());
546         }
547
548         FeatureApiUtils.apply(getDroolsProviders().getList(),
549             feature -> feature.afterInsert(this, event, successInject),
550             (feature, ex) -> logger.error("{}: feature {} after-insert failure because of {}", this,
551                             feature.getClass().getName(), ex.getMessage(), ex));
552
553         return true;
554
555     }
556
557     @Override
558     public boolean deliver(TopicSink sink, Object event) {
559
560         logger.info("{}DELIVER: {} FROM {} TO {}", this, event, this, sink);
561
562         for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
563             try {
564                 if (feature.beforeDeliver(this, sink, event)) {
565                     return true;
566                 }
567             } catch (Exception e) {
568                 logger.error("{}: feature {} before-deliver failure because of {}", this, feature.getClass().getName(),
569                         e.getMessage(), e);
570             }
571         }
572
573         if (sink == null) {
574             throw new IllegalArgumentException(this +  " invalid sink");
575         }
576
577         if (event == null) {
578             throw new IllegalArgumentException(this +  " invalid event");
579         }
580
581         if (this.locked) {
582             throw new IllegalStateException(this +  " is locked");
583         }
584
585         if (!this.alive) {
586             throw new IllegalStateException(this +  " is stopped");
587         }
588
589         String json =
590                 getCoderManager().encode(sink.getTopic(), event, this);
591
592         synchronized (this.recentSinkEvents) {
593             this.recentSinkEvents.add(json);
594         }
595
596         boolean success = sink.send(json);
597
598         for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
599             try {
600                 if (feature.afterDeliver(this, sink, event, json, success)) {
601                     return true;
602                 }
603             } catch (Exception e) {
604                 logger.error("{}: feature {} after-deliver failure because of {}", this, feature.getClass().getName(),
605                         e.getMessage(), e);
606             }
607         }
608
609         return success;
610
611     }
612
613     @Override
614     public String getVersion() {
615         return this.policyContainer.getVersion();
616     }
617
618     @Override
619     public String getArtifactId() {
620         return this.policyContainer.getArtifactId();
621     }
622
623     @Override
624     public String getGroupId() {
625         return this.policyContainer.getGroupId();
626     }
627
628     /**
629      * Get model class loader hash.
630      *
631      * @return the modelClassLoaderHash
632      */
633     public int getModelClassLoaderHash() {
634         return modelClassLoaderHash;
635     }
636
637     @Override
638     public synchronized boolean lock() {
639         logger.info("LOCK: {}",  this);
640
641         this.locked = true;
642         return true;
643     }
644
645     @Override
646     public synchronized boolean unlock() {
647         logger.info("UNLOCK: {}",  this);
648
649         this.locked = false;
650         return true;
651     }
652
653     @Override
654     public boolean isLocked() {
655         return this.locked;
656     }
657
658     @GsonJsonIgnore
659     @Override
660     public PolicyContainer getContainer() {
661         return this.policyContainer;
662     }
663
664     @GsonJsonProperty("sessions")
665     @Override
666     public List<String> getSessionNames() {
667         return getSessionNames(true);
668     }
669
670     /**
671      * get session names.
672      *
673      * @param abbreviated true for the short form, otherwise the long form
674      * @return session names
675      */
676     protected List<String> getSessionNames(boolean abbreviated) {
677         List<String> sessionNames = new ArrayList<>();
678         try {
679             for (PolicySession session: this.policyContainer.getPolicySessions()) {
680                 if (abbreviated) {
681                     sessionNames.add(session.getName());
682                 } else {
683                     sessionNames.add(session.getFullName());
684                 }
685             }
686         } catch (Exception e) {
687             logger.warn("Can't retrieve CORE sessions", e);
688             sessionNames.add(e.getMessage());
689         }
690         return sessionNames;
691     }
692
693     @GsonJsonProperty("sessionCoordinates")
694     @Override
695     public List<String> getCanonicalSessionNames() {
696         return getSessionNames(false);
697     }
698
699     @Override
700     public List<String> getBaseDomainNames() {
701         return new ArrayList<>(this.policyContainer.getKieContainer().getKieBaseNames());
702     }
703
704     /**
705      * provides the underlying core layer container sessions.
706      *
707      * @return the attached Policy Container
708      */
709     protected List<PolicySession> getSessions() {
710         List<PolicySession> sessions = new ArrayList<>();
711         sessions.addAll(this.policyContainer.getPolicySessions());
712         return sessions;
713     }
714
715     /**
716      * provides the underlying core layer container session with name sessionName.
717      *
718      * @param sessionName session name
719      * @return the attached Policy Container
720      * @throws IllegalArgumentException when an invalid session name is provided
721      * @throws IllegalStateException when the drools controller is in an invalid state
722      */
723     protected PolicySession getSession(String sessionName) {
724         if (sessionName == null || sessionName.isEmpty()) {
725             throw new IllegalArgumentException("A Session Name must be provided");
726         }
727
728         List<PolicySession> sessions = this.getSessions();
729         for (PolicySession session : sessions) {
730             if (sessionName.equals(session.getName()) || sessionName.equals(session.getFullName())) {
731                 return session;
732             }
733         }
734
735         throw invalidSessNameEx(sessionName);
736     }
737
738     private IllegalArgumentException invalidSessNameEx(String sessionName) {
739         return new IllegalArgumentException("Invalid Session Name: " + sessionName);
740     }
741
742     @Override
743     public Map<String, Integer> factClassNames(String sessionName) {
744         validateSessionName(sessionName);
745
746         Map<String, Integer> classNames = new HashMap<>();
747
748         PolicySession session = getSession(sessionName);
749         KieSession kieSession = session.getKieSession();
750
751         Collection<FactHandle> facts = kieSession.getFactHandles();
752         for (FactHandle fact : facts) {
753             try {
754                 String className = kieSession.getObject(fact).getClass().getName();
755                 if (classNames.containsKey(className)) {
756                     classNames.put(className, classNames.get(className) + 1);
757                 } else {
758                     classNames.put(className, 1);
759                 }
760             } catch (Exception e) {
761                 logger.warn(FACT_RETRIEVE_ERROR, fact, e);
762             }
763         }
764
765         return classNames;
766     }
767
768     private void validateSessionName(String sessionName) {
769         if (sessionName == null || sessionName.isEmpty()) {
770             throw invalidSessNameEx(sessionName);
771         }
772     }
773
774     @Override
775     public long factCount(String sessionName) {
776         validateSessionName(sessionName);
777
778         PolicySession session = getSession(sessionName);
779         return session.getKieSession().getFactCount();
780     }
781
782     @Override
783     public List<Object> facts(String sessionName, String className, boolean delete) {
784         validateSessionName(sessionName);
785
786         if (className == null || className.isEmpty()) {
787             throw new IllegalArgumentException("Invalid Class Name: " + className);
788         }
789
790         Class<?> factClass =
791                 ReflectionUtil.fetchClass(this.policyContainer.getClassLoader(), className);
792         if (factClass == null) {
793             throw new IllegalArgumentException("Class cannot be fetched in model's classloader: " + className);
794         }
795
796         PolicySession session = getSession(sessionName);
797         KieSession kieSession = session.getKieSession();
798
799         List<Object> factObjects = new ArrayList<>();
800
801         Collection<FactHandle> factHandles = kieSession.getFactHandles(new ClassObjectFilter(factClass));
802         for (FactHandle factHandle : factHandles) {
803             try {
804                 factObjects.add(kieSession.getObject(factHandle));
805                 if (delete) {
806                     kieSession.delete(factHandle);
807                 }
808             } catch (Exception e) {
809                 logger.warn(FACT_RETRIEVE_ERROR, factHandle, e);
810             }
811         }
812
813         return factObjects;
814     }
815
816     @Override
817     public <T> List<T> facts(@NonNull String sessionName, @NonNull Class<T> clazz) {
818         return facts(sessionName, clazz.getName(), false)
819             .stream()
820             .filter(clazz::isInstance)
821             .map(clazz::cast)
822             .collect(Collectors.toList());
823     }
824
825     @Override
826     public List<Object> factQuery(String sessionName, String queryName, String queriedEntity,
827             boolean delete, Object... queryParams) {
828         validateSessionName(sessionName);
829
830         if (queryName == null || queryName.isEmpty()) {
831             throw new IllegalArgumentException("Invalid Query Name: " + queryName);
832         }
833
834         if (queriedEntity == null || queriedEntity.isEmpty()) {
835             throw new IllegalArgumentException("Invalid Queried Entity: " + queriedEntity);
836         }
837
838         PolicySession session = getSession(sessionName);
839         KieSession kieSession = session.getKieSession();
840
841         validateQueryName(kieSession, queryName);
842
843         List<Object> factObjects = new ArrayList<>();
844
845         QueryResults queryResults = kieSession.getQueryResults(queryName, queryParams);
846         for (QueryResultsRow row : queryResults) {
847             try {
848                 factObjects.add(row.get(queriedEntity));
849                 if (delete) {
850                     kieSession.delete(row.getFactHandle(queriedEntity));
851                 }
852             } catch (Exception e) {
853                 logger.warn("Object cannot be retrieved from row: {}", row, e);
854             }
855         }
856
857         return factObjects;
858     }
859
860     private void validateQueryName(KieSession kieSession, String queryName) {
861         for (KiePackage kiePackage : kieSession.getKieBase().getKiePackages()) {
862             for (Query q : kiePackage.getQueries()) {
863                 if (q.getName() != null && q.getName().equals(queryName)) {
864                     return;
865                 }
866             }
867         }
868
869         throw new IllegalArgumentException("Invalid Query Name: " + queryName);
870     }
871
872     @Override
873     public <T> boolean delete(@NonNull String sessionName, @NonNull T objFact) {
874         KieSession kieSession = getSession(sessionName).getKieSession();
875
876         // try first to get the object to delete first by reference
877
878         FactHandle quickFact = kieSession.getFactHandle(objFact);
879         if (quickFact != null) {
880             logger.info("Fast delete of {} from {}", objFact, sessionName);
881             kieSession.delete(quickFact);
882             return true;
883         }
884
885         // otherwise, try to the delete the fact associated with the object by scanning all
886         // facts from the same type and performing object equality.   The set of facts
887         // is restricted to those of the same type
888
889         Collection<FactHandle> factHandles = kieSession.getFactHandles(new ClassObjectFilter(objFact.getClass()));
890         for (FactHandle factHandle : factHandles) {
891             if (Objects.equals(objFact, kieSession.getObject(factHandle))) {
892                 logger.info("Slow delete of {} of type {} from {}",
893                         objFact, objFact.getClass().getName(), sessionName);
894                 kieSession.delete(factHandle);
895                 return true;
896             }
897         }
898
899         return false;
900     }
901
902     @Override
903     public <T> boolean delete(@NonNull T fact) {
904         return this.getSessionNames().stream().map(ss -> delete(ss, fact)).reduce(false, Boolean::logicalOr);
905     }
906
907     @Override
908     public <T> boolean delete(@NonNull String sessionName, @NonNull Class<T> fact) {
909         PolicySession session = getSession(sessionName);
910         KieSession kieSession = session.getKieSession();
911
912         boolean success = true;
913         Collection<FactHandle> factHandles = kieSession.getFactHandles(new ClassObjectFilter(fact));
914         for (FactHandle factHandle : factHandles) {
915             try {
916                 kieSession.delete(factHandle);
917             } catch (Exception e) {
918                 logger.warn(FACT_RETRIEVE_ERROR, factHandle, e);
919                 success = false;
920             }
921         }
922         return success;
923     }
924
925     @Override
926     public <T> boolean delete(@NonNull Class<T> fact) {
927         return this.getSessionNames().stream().map(ss -> delete(ss, fact)).reduce(false, Boolean::logicalOr);
928     }
929
930     @Override
931     public <T> boolean exists(@NonNull String sessionName, @NonNull T objFact) {
932         KieSession kieSession = getSession(sessionName).getKieSession();
933         if (kieSession.getFactHandle(objFact) != null) {
934             return true;
935         }
936
937         // try to find the object by equality comparison instead if it could not be
938         // found by reference
939
940         Collection<FactHandle> factHandles = kieSession.getFactHandles(new ClassObjectFilter(objFact.getClass()));
941         for (FactHandle factHandle : factHandles) {
942             if (Objects.equals(objFact, kieSession.getObject(factHandle))) {
943                 return true;
944             }
945         }
946
947         return false;
948     }
949
950     @Override
951     public <T> boolean exists(@NonNull T fact) {
952         return this.getSessionNames().stream().anyMatch(ss -> exists(ss, fact));
953     }
954
955     @Override
956     public Class<?> fetchModelClass(String className) {
957         return ReflectionUtil.fetchClass(this.policyContainer.getClassLoader(), className);
958     }
959
960     /**
961      * Get recent source events.
962      *
963      * @return the recentSourceEvents
964      */
965     @Override
966     public Object[] getRecentSourceEvents() {
967         synchronized (this.recentSourceEvents) {
968             Object[] events = new Object[recentSourceEvents.size()];
969             return recentSourceEvents.toArray(events);
970         }
971     }
972
973     /**
974      * Get recent sink events.
975      *
976      * @return the recentSinkEvents
977      */
978     @Override
979     public String[] getRecentSinkEvents() {
980         synchronized (this.recentSinkEvents) {
981             String[] events = new String[recentSinkEvents.size()];
982             return recentSinkEvents.toArray(events);
983         }
984     }
985
986     @Override
987     public boolean isBrained() {
988         return true;
989     }
990
991
992     @Override
993     public String toString() {
994         StringBuilder builder = new StringBuilder();
995         builder
996             .append("MavenDroolsController [policyContainer=")
997             .append(policyContainer.getName())
998             .append(":")
999             .append(", alive=")
1000             .append(alive)
1001             .append(", locked=")
1002             .append(", modelClassLoaderHash=")
1003             .append(modelClassLoaderHash)
1004             .append("]");
1005         return builder.toString();
1006     }
1007
1008     // these may be overridden by junit tests
1009
1010     protected EventProtocolCoder getCoderManager() {
1011         return EventProtocolCoderConstants.getManager();
1012     }
1013
1014     protected OrderedServiceImpl<DroolsControllerFeatureApi> getDroolsProviders() {
1015         return DroolsControllerFeatureApiConstants.getProviders();
1016     }
1017
1018     protected PolicyContainer makePolicyContainer(String groupId, String artifactId, String version) {
1019         return new PolicyContainer(groupId, artifactId, version);
1020     }
1021
1022     protected boolean isClass(String className) {
1023         return ReflectionUtil.isClass(this.policyContainer.getClassLoader(), className);
1024     }
1025 }