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