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