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