From 1320df00310f70b3cf6695466eb2d76a2d3dc49b Mon Sep 17 00:00:00 2001 From: "Straubs, Ralph (rs8887)" Date: Mon, 11 Nov 2019 11:28:47 -0600 Subject: Add 'DroolsRunnable' class 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) --- .../onap/policy/drools/core/DroolsRunnable.java | 28 +++++++ .../onap/policy/drools/core/PolicyContainer.java | 43 ++++++++++ .../java/org/onap/policy/drools/util/KieUtils.java | 95 ++++++++++++++++++++++ policy-core/src/main/resources/META-INF/drools/drl | 40 +++++++++ .../policy/drools/core/DroolsContainerTest.java | 12 +++ .../org/onap/policy/drools/util/KieUtilsTest.java | 62 +++++++++++++- .../MavenDroolsControllerUpgradesTest.java | 6 +- 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/DroolsRunnable.java create mode 100644 policy-core/src/main/resources/META-INF/drools/drl 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 index 00000000..18e66e90 --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/DroolsRunnable.java @@ -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 { +} diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/PolicyContainer.java b/policy-core/src/main/java/org/onap/policy/drools/core/PolicyContainer.java index 4e1b1d6c..0fe1f855 100644 --- a/policy-core/src/main/java/org/onap/policy/drools/core/PolicyContainer.java +++ b/policy-core/src/main/java/org/onap/policy/drools/core/PolicyContainer.java @@ -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 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 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); + } + } } diff --git a/policy-core/src/main/java/org/onap/policy/drools/util/KieUtils.java b/policy-core/src/main/java/org/onap/policy/drools/util/KieUtils.java index 03a307cf..bd1c6cee 100644 --- a/policy-core/src/main/java/org/onap/policy/drools/util/KieUtils.java +++ b/policy-core/src/main/java/org/onap/policy/drools/util/KieUtils.java @@ -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 resourceToPackages(ClassLoader classLoader, String resourceName) { + + // find all resources matching 'resourceName' + Enumeration 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 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 kiePackages) { + HashSet 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 index 00000000..949f235e --- /dev/null +++ b/policy-core/src/main/resources/META-INF/drools/drl @@ -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 diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/DroolsContainerTest.java b/policy-core/src/test/java/org/onap/policy/drools/core/DroolsContainerTest.java index d168ca8b..2f45f01e 100644 --- a/policy-core/src/test/java/org/onap/policy/drools/core/DroolsContainerTest.java +++ b/policy-core/src/test/java/org/onap/policy/drools/core/DroolsContainerTest.java @@ -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 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()); diff --git a/policy-core/src/test/java/org/onap/policy/drools/util/KieUtilsTest.java b/policy-core/src/test/java/org/onap/policy/drools/util/KieUtilsTest.java index 83f62b32..89ab9d0a 100644 --- a/policy-core/src/test/java/org/onap/policy/drools/util/KieUtilsTest.java +++ b/policy-core/src/test/java/org/onap/policy/drools/util/KieUtilsTest.java @@ -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 getResources(String name) throws IOException { + if ("BogusUrl".equals(name)) { + return new Enumeration() { + @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"); + } + } + } +} diff --git a/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerUpgradesTest.java b/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerUpgradesTest.java index 604ddad6..8c8cf977 100644 --- a/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerUpgradesTest.java +++ b/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerUpgradesTest.java @@ -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() { -- cgit 1.2.3-korg