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