Add 'DroolsRunnable' class 53/98253/5
authorStraubs, Ralph (rs8887) <rs8887@att.com>
Mon, 11 Nov 2019 17:28:47 +0000 (11:28 -0600)
committerStraubs, Ralph (rs8887) <rs8887@att.com>
Thu, 14 Nov 2019 09:07:25 +0000 (03:07 -0600)
This provides a simple way to run arbitrary Java code within the
Drools thread. This change also includes a general way to specify
Drools rules that are automatically added to every Drools session.

Change-Id: I5ddcca4c807dc552fbcbd4a19dce311a4d358279
Issue-ID: POLICY-1948
Signed-off-by: Straubs, Ralph (rs8887) <rs8887@att.com>
policy-core/src/main/java/org/onap/policy/drools/core/DroolsRunnable.java [new file with mode: 0644]
policy-core/src/main/java/org/onap/policy/drools/core/PolicyContainer.java
policy-core/src/main/java/org/onap/policy/drools/util/KieUtils.java
policy-core/src/main/resources/META-INF/drools/drl [new file with mode: 0644]
policy-core/src/test/java/org/onap/policy/drools/core/DroolsContainerTest.java
policy-core/src/test/java/org/onap/policy/drools/util/KieUtilsTest.java
policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerUpgradesTest.java

diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/DroolsRunnable.java b/policy-core/src/main/java/org/onap/policy/drools/core/DroolsRunnable.java
new file mode 100644 (file)
index 0000000..18e66e9
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * ============LICENSE_START=======================================================
+ * policy-core
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.drools.core;
+
+/**
+ * This class provides the ability to execute arbitrary code within a
+ * Drools thread.
+ */
+public interface DroolsRunnable extends Runnable {
+}
index 4e1b1d6..0fe1f85 100644 (file)
@@ -33,9 +33,11 @@ import org.kie.api.builder.KieScanner;
 import org.kie.api.builder.Message;
 import org.kie.api.builder.ReleaseId;
 import org.kie.api.builder.Results;
+import org.kie.api.definition.KiePackage;
 import org.kie.api.runtime.KieContainer;
 import org.kie.api.runtime.KieSession;
 import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.drools.util.KieUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -74,6 +76,12 @@ public class PolicyContainer implements Startable {
 
     private static final String ERROR_STRING = "ERROR: Feature API: ";
 
+    // packages that are included in all 'KieContainer' instances
+    private static Collection<KiePackage> commonPackages = null;
+
+    // all resources with this name consist of rules that are added to each container
+    private static final String COMMON_PACKAGES_RESOURCE_NAME = "META-INF/drools/drl";
+
     /**
      * uses 'groupId', 'artifactId' and 'version', and fetches the associated artifact and remaining
      * dependencies from the Maven repository to create the 'PolicyContainer' and associated
@@ -107,6 +115,9 @@ public class PolicyContainer implements Startable {
         } else {
             kieContainer = kieServices.newKieContainer(newReleaseId);
         }
+
+        // add common KiePackage instances
+        addCommonPackages();
         synchronized (containers) {
             if (newReleaseId != null) {
                 logger.info("Add a new kieContainer in containers: releaseId: {}", newReleaseId);
@@ -401,6 +412,10 @@ public class PolicyContainer implements Startable {
         // update the version
         Results results = kieContainer.updateToVersion(releaseId);
 
+
+        // add common KiePackage instances
+        addCommonPackages();
+
         // restart all session threads, and notify the sessions
         for (PolicySession session : sessions.values()) {
             session.startThread();
@@ -726,4 +741,32 @@ public class PolicyContainer implements Startable {
             adjuncts.put(object, value);
         }
     }
+
+    /**
+     * Add 'KiePackages' that are common to all containers.
+     */
+    private void addCommonPackages() {
+        // contains the list of 'KiePackages' to add to each 'KieBase'
+        Collection<KiePackage> kiePackages;
+        synchronized (PolicyContainer.class) {
+            if (commonPackages == null) {
+                commonPackages = KieUtils.resourceToPackages(
+                    PolicyContainer.class.getClassLoader(), COMMON_PACKAGES_RESOURCE_NAME);
+                if (commonPackages == null) {
+                    // a problem occurred, which has already been logged --
+                    // just store an empty collection, so we don't keep doing
+                    // this over again
+                    commonPackages = new HashSet<>();
+                    return;
+                }
+            }
+            kiePackages = commonPackages;
+        }
+
+        // if we reach this point, 'kiePackages' contains a non-null list
+        // of packages to add
+        for (String name : kieContainer.getKieBaseNames()) {
+            KieUtils.addKiePackages(kieContainer.getKieBase(name), kiePackages);
+        }
+    }
 }
index 03a307c..bd1c6ce 100644 (file)
@@ -22,14 +22,25 @@ package org.onap.policy.drools.util;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.List;
 import java.util.stream.Collectors;
 import lombok.NonNull;
+import org.apache.commons.io.IOUtils;
 import org.drools.compiler.kie.builder.impl.InternalKieModule;
 import org.drools.compiler.kproject.models.KieModuleModelImpl;
+import org.drools.core.impl.KnowledgeBaseImpl;
+import org.kie.api.KieBase;
 import org.kie.api.KieServices;
 import org.kie.api.builder.KieBuilder;
 import org.kie.api.builder.KieFileSystem;
@@ -41,12 +52,20 @@ import org.kie.api.definition.rule.Rule;
 import org.kie.api.runtime.KieContainer;
 import org.kie.api.runtime.KieSession;
 import org.kie.scanner.KieMavenRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Kie related utilities.
  */
 public class KieUtils {
 
+    private static final Logger logger = LoggerFactory.getLogger(KieUtils.class);
+
+    // resource names used by 'resourceToPackages'
+    private static final String RESOURCE_PREFIX = "src/main/resources/drools";
+    private static final String RESOURCE_SUFFIX = ".drl";
+
     private KieUtils() {
         // Utility class
     }
@@ -154,4 +173,80 @@ public class KieUtils {
         }
         return kieBuilder;
     }
+
+    /**
+     * Find all Drools resources matching a specified name, and generate a
+     * collection of 'KiePackage' instances from those resources.
+     *
+     * @param classLoader the class loader to use when finding resources, or
+     *     when building the 'KiePackage' collection
+     * @param resourceName the resource name, without a leading '/' character
+     * @return a collection of 'KiePackage' instances, or 'null' in case of
+     *     failure
+     */
+    public static Collection<KiePackage> resourceToPackages(ClassLoader classLoader, String resourceName) {
+
+        // find all resources matching 'resourceName'
+        Enumeration<URL> resources;
+        try {
+            resources = classLoader.getResources(resourceName);
+        } catch (IOException e) {
+            logger.error("Exception fetching resources: " + resourceName, e);
+            return null;
+        }
+        if (!resources.hasMoreElements()) {
+            // no resources found
+            return null;
+        }
+
+        // generate a 'KieFileSystem' from these resources
+        KieServices kieServices = KieServices.Factory.get();
+        KieFileSystem kfs = kieServices.newKieFileSystem();
+        int index = 1;
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            try (InputStream is = url.openStream()) {
+                // convert a resource to a byte array
+                byte[] drl = IOUtils.toByteArray(is);
+
+                // add a new '.drl' entry to the KieFileSystem
+                kfs.write(RESOURCE_PREFIX + index++ + RESOURCE_SUFFIX, drl);
+            } catch (IOException e) {
+                logger.error("Couldn't read in " + url, e);
+                return null;
+            }
+        }
+
+        // do a build of the 'KieFileSystem'
+        KieBuilder builder = kieServices.newKieBuilder(kfs, classLoader);
+        builder.buildAll();
+        List<Message> results = builder.getResults().getMessages();
+        if (!results.isEmpty()) {
+            logger.error("Kie build failed:\n" + results);
+            return null;
+        }
+
+        // generate a KieContainer, and extract the package list
+        return kieServices.newKieContainer(builder.getKieModule().getReleaseId(), classLoader)
+               .getKieBase().getKiePackages();
+    }
+
+    /**
+     * Add a collection of 'KiePackage' instances to the specified 'KieBase'.
+     *
+     * @param kieBase the 'KieBase' instance to add the packages to
+     * @param kiePackages the collection of packages to add
+     */
+    public static void addKiePackages(KieBase kieBase, Collection<KiePackage> kiePackages) {
+        HashSet<KiePackage> stillNeeded = new HashSet<>(kiePackages);
+
+        // update 'stillNeeded' by removing any packages we already have
+        stillNeeded.removeAll(kieBase.getKiePackages());
+
+        if (!stillNeeded.isEmpty()) {
+            // there are still packages we need to add --
+            // this code makes use of an internal class and method
+            ((KnowledgeBaseImpl)kieBase).addPackages(stillNeeded);
+        }
+    }
 }
diff --git a/policy-core/src/main/resources/META-INF/drools/drl b/policy-core/src/main/resources/META-INF/drools/drl
new file mode 100644 (file)
index 0000000..949f235
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * policy-core
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.drools.core;
+
+// This rule will match any 'DroolsRunnable' instance placed in Drools
+// memory, and run the 'DroolsRunnable.run()' method. It provides a way
+// to run arbitrary Java code within a Drools session thread by inserting
+// a 'DroolsRunnable' instance into Drools memory.
+
+rule "run-drools-runnable"
+    when
+        $runnable : DroolsRunnable()
+    then
+    {
+      // we retract it first, because the 'run()' method may traverse
+      // Drools objects
+      retract($runnable);
+
+      // run the code within the Drools thread
+      $runnable.run();
+    }
+end
index d168ca8..2f45f01 100644 (file)
@@ -165,6 +165,18 @@ public class DroolsContainerTest {
             container.insert("session1", result);
 
             assertEquals(48, result.poll(TIMEOUT_SEC, TimeUnit.SECONDS).intValue());
+
+            // verify that default KiePackages have been added by testing
+            // that 'DroolsRunnable' is functioning
+
+            final LinkedBlockingQueue<String> lbq = new LinkedBlockingQueue<>();
+            container.insert("session1", new DroolsRunnable() {
+                    @Override
+                    public void run() {
+                        lbq.add("DroolsRunnable String");
+                    }
+                });
+            assertEquals("DroolsRunnable String", lbq.poll(TIMEOUT_SEC, TimeUnit.SECONDS));
         } finally {
             container.shutdown();
             assertFalse(container.isAlive());
index 83f62b3..89ab9d0 100644 (file)
@@ -22,12 +22,15 @@ package org.onap.policy.drools.util;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 
 import java.io.IOException;
+import java.net.URL;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Enumeration;
 import java.util.stream.Collectors;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -119,4 +122,61 @@ public class KieUtilsTest {
     public void getFacts() {
         assertEquals(0, KieUtils.getFacts(session).size());
     }
-}
\ No newline at end of file
+
+    @Test
+    public void resourceToPackagesTests() {
+        // Some minimal logging -- it would be nice to verify the 'KieUtils' logger messages
+        StringBuffer log;
+
+        // test IOException from ClassLoader
+        log = new StringBuffer();
+        assertNull(KieUtils.resourceToPackages(new BogusClassLoader(log), "BogusClassLoader"));
+        assertEquals("IOException(BogusClassLoader)", log.toString());
+
+        // test 'null' return when no resources are found
+        assertNull(KieUtils.resourceToPackages(ClassLoader.getSystemClassLoader(), "no/such/url"));
+
+        // test IOException in 'IOUtils.toByteArray()' -> 'InputStream.read()'
+        log = new StringBuffer();
+        assertNull(KieUtils.resourceToPackages(new BogusClassLoader(log), "BogusUrl"));
+        assertEquals("", log.toString());
+
+        // don't know how to test 'KieBuilder' errors at this point
+
+        // success legs are tested in 'DroolsContainerTest'
+    }
+
+    static class BogusClassLoader extends ClassLoader {
+        StringBuffer log;
+
+        BogusClassLoader(StringBuffer log) {
+            this.log = log;
+        }
+
+        @Override
+        public Enumeration<URL> getResources(String name) throws IOException {
+            if ("BogusUrl".equals(name)) {
+                return new Enumeration<URL>() {
+                    @Override
+                    public boolean hasMoreElements() {
+                        return true;
+                    }
+
+                    @Override
+                    public URL nextElement() {
+                        try {
+                            // when the following URL is used, an IOException will occur
+                            return new URL("http://127.0.0.1:1");
+                        } catch (IOException e) {
+                            // this should never happen, as the URL above is syntactically valid
+                            return null;
+                        }
+                    }
+                };
+            } else {
+                log.append("IOException(BogusClassLoader)");
+                throw new IOException("BogusClassLoader");
+            }
+        }
+    }
+}
index 604ddad..8c8cf97 100644 (file)
@@ -140,7 +140,7 @@ public class MavenDroolsControllerUpgradesTest {
 
         assertTrue(running1a.await(30, TimeUnit.SECONDS));
         summary();
-        assertKie(Arrays.asList("SETUP.1", "VERSION.12"), 1);
+        assertKie(Arrays.asList("run-drools-runnable", "SETUP.1", "VERSION.12"), 1);
 
         controller.updateToVersion(
             rulesDescriptor2.getGroupId(),
@@ -151,7 +151,7 @@ public class MavenDroolsControllerUpgradesTest {
         assertTrue(running2a.await(30, TimeUnit.SECONDS));
         assertTrue(running2b.await(30, TimeUnit.SECONDS));
         summary();
-        assertKie(Arrays.asList("SETUP.1", "VERSION.12", "SETUP.2", "VERSION.2"), 2);
+        assertKie(Arrays.asList("run-drools-runnable", "SETUP.1", "VERSION.12", "SETUP.2", "VERSION.2"), 2);
 
         controller.updateToVersion(
             rulesDescriptor1.getGroupId(),
@@ -161,7 +161,7 @@ public class MavenDroolsControllerUpgradesTest {
 
         assertTrue(running1b.await(30, TimeUnit.SECONDS));
         summary();
-        assertKie(Arrays.asList("SETUP.1", "VERSION.12"), 1);
+        assertKie(Arrays.asList("run-drools-runnable", "SETUP.1", "VERSION.12"), 1);
     }
 
     private void summary() {