a6459d67395a1471095cf0109a5fd9f15b6d7e73
[policy/drools-pdp.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2019-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;
23
24 import com.google.re2j.Pattern;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Properties;
30 import lombok.NonNull;
31 import org.apache.commons.lang3.StringUtils;
32 import org.onap.policy.common.endpoints.event.comm.Topic;
33 import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
34 import org.onap.policy.common.endpoints.event.comm.TopicSink;
35 import org.onap.policy.common.endpoints.event.comm.TopicSource;
36 import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties;
37 import org.onap.policy.common.utils.services.FeatureApiUtils;
38 import org.onap.policy.drools.controller.internal.MavenDroolsController;
39 import org.onap.policy.drools.controller.internal.NullDroolsController;
40 import org.onap.policy.drools.features.DroolsControllerFeatureApi;
41 import org.onap.policy.drools.features.DroolsControllerFeatureApiConstants;
42 import org.onap.policy.drools.properties.DroolsPropertyConstants;
43 import org.onap.policy.drools.protocol.coders.JsonProtocolFilter;
44 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration;
45 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.CustomGsonCoder;
46 import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.PotentialCoderFilter;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * Factory of Drools Controllers indexed by the Maven coordinates.
52  */
53 class IndexedDroolsControllerFactory implements DroolsControllerFactory {
54
55     private static final Logger logger = LoggerFactory.getLogger(IndexedDroolsControllerFactory.class);
56     private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*");
57
58     /**
59      * Policy Controller Name Index.
60      */
61     protected Map<String, DroolsController> droolsControllers = new HashMap<>();
62
63     /**
64      * Null Drools Controller.
65      */
66     protected NullDroolsController nullDroolsController;
67
68     /**
69      * Constructs the object.
70      */
71     public IndexedDroolsControllerFactory() {
72
73         /* Add a NULL controller which will always be present in the hash */
74
75         nullDroolsController = new NullDroolsController();
76         String controllerId = nullDroolsController.getGroupId() + ":" + nullDroolsController.getArtifactId();
77
78         synchronized (this) {
79             droolsControllers.put(controllerId, nullDroolsController);
80         }
81     }
82
83     @Override
84     public DroolsController build(Properties properties, List<? extends TopicSource> eventSources,
85             List<? extends TopicSink> eventSinks) throws LinkageError {
86
87         String groupId = properties.getProperty(DroolsPropertyConstants.RULES_GROUPID);
88         if (StringUtils.isBlank(groupId)) {
89             groupId = DroolsControllerConstants.NO_GROUP_ID;
90         }
91
92         String artifactId = properties.getProperty(DroolsPropertyConstants.RULES_ARTIFACTID);
93         if (StringUtils.isBlank(artifactId)) {
94             artifactId = DroolsControllerConstants.NO_ARTIFACT_ID;
95         }
96
97         String version = properties.getProperty(DroolsPropertyConstants.RULES_VERSION);
98         if (StringUtils.isBlank(version)) {
99             version = DroolsControllerConstants.NO_VERSION;
100         }
101
102         List<TopicCoderFilterConfiguration> topics2DecodedClasses2Filters = codersAndFilters(properties, eventSources);
103         List<TopicCoderFilterConfiguration> topics2EncodedClasses2Filters = codersAndFilters(properties, eventSinks);
104
105         return this.build(properties, groupId, artifactId, version,
106                 topics2DecodedClasses2Filters, topics2EncodedClasses2Filters);
107     }
108
109     @Override
110     public DroolsController build(Properties properties, String newGroupId, String newArtifactId, String newVersion,
111             List<TopicCoderFilterConfiguration> decoderConfigurations,
112             List<TopicCoderFilterConfiguration> encoderConfigurations) throws LinkageError {
113
114         if (StringUtils.isBlank(newGroupId)) {
115             throw new IllegalArgumentException("Missing maven group-id coordinate");
116         }
117
118         if (StringUtils.isBlank(newArtifactId)) {
119             throw new IllegalArgumentException("Missing maven artifact-id coordinate");
120         }
121
122         if (StringUtils.isBlank(newVersion)) {
123             throw new IllegalArgumentException("Missing maven version coordinate");
124         }
125
126         String controllerId = newGroupId + ":" + newArtifactId;
127         DroolsController controllerCopy = null;
128         synchronized (this) {
129             /*
130              * The Null Drools Controller for no maven coordinates is always here so when no
131              * coordinates present, this is the return point
132              *
133              * assert (controllerCopy instanceof NullDroolsController)
134              */
135             if (droolsControllers.containsKey(controllerId)) {
136                 controllerCopy = droolsControllers.get(controllerId);
137                 if (controllerCopy.getVersion().equalsIgnoreCase(newVersion)) {
138                     return controllerCopy;
139                 }
140             }
141         }
142
143         if (controllerCopy != null) {
144             /*
145              * a controller keyed by group id + artifact id exists but with different version =>
146              * version upgrade/downgrade
147              */
148
149             controllerCopy.updateToVersion(newGroupId, newArtifactId, newVersion, decoderConfigurations,
150                     encoderConfigurations);
151
152             return controllerCopy;
153         }
154
155         /* new drools controller */
156
157         DroolsController controller = applyBeforeInstance(properties, newGroupId, newArtifactId, newVersion,
158                         decoderConfigurations, encoderConfigurations);
159
160         if (controller == null) {
161             controller = new MavenDroolsController(newGroupId, newArtifactId, newVersion, decoderConfigurations,
162                     encoderConfigurations);
163         }
164
165         synchronized (this) {
166             droolsControllers.put(controllerId, controller);
167         }
168
169         final DroolsController controllerFinal = controller;
170
171         FeatureApiUtils.apply(getProviders(),
172             feature -> feature.afterInstance(controllerFinal, properties),
173             (feature, ex) -> logger.error("feature {} ({}) afterInstance() of drools controller {}:{}:{} failed",
174                             feature.getName(), feature.getSequenceNumber(),
175                             newGroupId, newArtifactId, newVersion, ex));
176
177         return controller;
178     }
179
180     private DroolsController applyBeforeInstance(Properties properties, String newGroupId, String newArtifactId,
181                     String newVersion, List<TopicCoderFilterConfiguration> decoderConfigurations,
182                     List<TopicCoderFilterConfiguration> encoderConfigurations) {
183         DroolsController controller = null;
184         for (DroolsControllerFeatureApi feature: getProviders()) {
185             try {
186                 controller = feature.beforeInstance(properties,
187                         newGroupId, newArtifactId, newVersion,
188                         decoderConfigurations, encoderConfigurations);
189                 if (controller != null) {
190                     logger.info("feature {} ({}) beforeInstance() has intercepted drools controller {}:{}:{}",
191                             feature.getName(), feature.getSequenceNumber(),
192                             newGroupId, newArtifactId, newVersion);
193                     break;
194                 }
195             } catch (RuntimeException r) {
196                 logger.error("feature {} ({}) beforeInstance() of drools controller {}:{}:{} failed",
197                         feature.getName(), feature.getSequenceNumber(),
198                         newGroupId, newArtifactId, newVersion, r);
199             }
200         }
201         return controller;
202     }
203
204     protected List<DroolsControllerFeatureApi> getProviders() {
205         return DroolsControllerFeatureApiConstants.getProviders().getList();
206     }
207
208     /**
209      * find out decoder classes and filters.
210      *
211      * @param properties properties with information about decoders
212      * @param topicEntities topic sources
213      * @return list of topics, each with associated decoder classes, each with a list of associated
214      *         filters
215      * @throws IllegalArgumentException invalid input data
216      */
217     protected List<TopicCoderFilterConfiguration> codersAndFilters(Properties properties,
218             List<? extends Topic> topicEntities) {
219
220         List<TopicCoderFilterConfiguration> topics2DecodedClasses2Filters = new ArrayList<>();
221
222         if (topicEntities == null || topicEntities.isEmpty()) {
223             return topics2DecodedClasses2Filters;
224         }
225
226         for (Topic topic : topicEntities) {
227
228             // 1. first the topic
229
230             String firstTopic = topic.getTopic();
231
232             String propertyTopicEntityPrefix = getPropertyTopicPrefix(topic) + firstTopic;
233
234             // 2. check if there is a custom decoder for this topic that the user prefers to use
235             // instead of the ones provided in the platform
236
237             var customGsonCoder = getCustomCoder(properties, propertyTopicEntityPrefix);
238
239             // 3. second the list of classes associated with each topic
240
241             String eventClasses = properties
242                     .getProperty(propertyTopicEntityPrefix + PolicyEndPointProperties.PROPERTY_TOPIC_EVENTS_SUFFIX);
243
244             if (StringUtils.isBlank(eventClasses)) {
245                 logger.warn("There are no event classes for topic {}", firstTopic);
246                 continue;
247             }
248
249             List<PotentialCoderFilter> classes2Filters =
250                             getFilterExpressions(properties, propertyTopicEntityPrefix, eventClasses);
251
252             topics2DecodedClasses2Filters
253                     .add(new TopicCoderFilterConfiguration(firstTopic, classes2Filters, customGsonCoder));
254         }
255
256         return topics2DecodedClasses2Filters;
257     }
258
259     private String getPropertyTopicPrefix(Topic topic) {
260         boolean isSource = topic instanceof TopicSource;
261         var commInfra = topic.getTopicCommInfrastructure();
262         if (commInfra == CommInfrastructure.NOOP) {
263             if (isSource) {
264                 return PolicyEndPointProperties.PROPERTY_NOOP_SOURCE_TOPICS + ".";
265             } else {
266                 return PolicyEndPointProperties.PROPERTY_NOOP_SINK_TOPICS + ".";
267             }
268         } else if (commInfra == CommInfrastructure.KAFKA) {
269             if (isSource) {
270                 return PolicyEndPointProperties.PROPERTY_KAFKA_SOURCE_TOPICS + ".";
271             } else {
272                 return PolicyEndPointProperties.PROPERTY_KAFKA_SINK_TOPICS + ".";
273             }
274         } else {
275             throw new IllegalArgumentException("Invalid Communication Infrastructure: " + commInfra);
276         }
277     }
278
279     private CustomGsonCoder getCustomCoder(Properties properties, String propertyPrefix) {
280         String customGson = properties.getProperty(propertyPrefix
281                 + PolicyEndPointProperties.PROPERTY_TOPIC_EVENTS_CUSTOM_MODEL_CODER_GSON_SUFFIX);
282
283         CustomGsonCoder customGsonCoder = null;
284         if (StringUtils.isNotBlank(customGson)) {
285             try {
286                 customGsonCoder = new CustomGsonCoder(customGson);
287             } catch (IllegalArgumentException e) {
288                 logger.warn("{}: cannot create custom-gson-coder {} because of {}", this, customGson,
289                         e.getMessage(), e);
290             }
291         }
292         return customGsonCoder;
293     }
294
295     private List<PotentialCoderFilter> getFilterExpressions(Properties properties, String propertyPrefix,
296                     @NonNull String eventClasses) {
297
298         List<PotentialCoderFilter> classes2Filters = new ArrayList<>();
299         for (String theClass : COMMA_SPACE_PAT.split(eventClasses)) {
300
301             // 4. for each coder class, get the filter expression
302
303             String filter = properties
304                     .getProperty(propertyPrefix
305                             + PolicyEndPointProperties.PROPERTY_TOPIC_EVENTS_SUFFIX
306                             + "." + theClass + PolicyEndPointProperties.PROPERTY_TOPIC_EVENTS_FILTER_SUFFIX);
307
308             var class2Filters = new PotentialCoderFilter(theClass, new JsonProtocolFilter(filter));
309             classes2Filters.add(class2Filters);
310         }
311
312         return classes2Filters;
313     }
314
315     @Override
316     public void destroy(DroolsController controller) {
317         unmanage(controller);
318         controller.halt();
319     }
320
321     @Override
322     public void destroy() {
323         List<DroolsController> controllers = this.inventory();
324         for (DroolsController controller : controllers) {
325             controller.halt();
326         }
327
328         synchronized (this) {
329             this.droolsControllers.clear();
330         }
331     }
332
333     /**
334      * unmanage the drools controller.
335      *
336      * @param controller the controller
337      */
338     protected void unmanage(DroolsController controller) {
339         if (controller == null) {
340             throw new IllegalArgumentException("No controller provided");
341         }
342
343         if (!controller.isBrained()) {
344             logger.info("Drools Controller is NOT OPERATIONAL - nothing to destroy");
345             return;
346         }
347
348         String controllerId = controller.getGroupId() + ":" + controller.getArtifactId();
349         synchronized (this) {
350             if (!this.droolsControllers.containsKey(controllerId)) {
351                 return;
352             }
353
354             droolsControllers.remove(controllerId);
355         }
356     }
357
358     @Override
359     public void shutdown(DroolsController controller) {
360         this.unmanage(controller);
361         controller.shutdown();
362     }
363
364     @Override
365     public void shutdown() {
366         List<DroolsController> controllers = this.inventory();
367         for (DroolsController controller : controllers) {
368             controller.shutdown();
369         }
370
371         synchronized (this) {
372             this.droolsControllers.clear();
373         }
374     }
375
376     @Override
377     public DroolsController get(String groupId, String artifactId, String version) {
378
379         if (StringUtils.isBlank(groupId) || StringUtils.isBlank(artifactId)) {
380             throw new IllegalArgumentException("Missing maven coordinates: " + groupId + ":" + artifactId);
381         }
382
383         String controllerId = groupId + ":" + artifactId;
384
385         synchronized (this) {
386             if (this.droolsControllers.containsKey(controllerId)) {
387                 return droolsControllers.get(controllerId);
388             } else {
389                 throw new IllegalStateException("DroolController for " + controllerId + " not found");
390             }
391         }
392     }
393
394     @Override
395     public List<DroolsController> inventory() {
396         return new ArrayList<>(this.droolsControllers.values());
397     }
398
399     @Override
400     public String toString() {
401         return "IndexedDroolsControllerFactory [#droolsControllers=" + droolsControllers.size() + "]";
402     }
403
404 }