FELIX-6542 ScriptedHealthCheck should handle ScriptEngineFactory defined in a fragmen... master
authorEric Norman <enorman@apache.org>
Tue, 28 Jun 2022 03:58:38 +0000 (20:58 -0700)
committerGitHub <noreply@github.com>
Tue, 28 Jun 2022 03:58:38 +0000 (06:58 +0300)
* FELIX-6542 handle ScriptEngineFactory defined in a fragment bundle

* FELIX-6542 fix spelling

* FELIX-6542 updated language description

groovy-all bundle doesn't exist after version 2.5 so change the text to
groovy-jsr223

healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheck.java
healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTracker.java
healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/DummyScriptEngineFactory.java [new file with mode: 0644]
healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTrackerTest.java [new file with mode: 0644]

index 36a203f2cb19c5b1e088096979d76ab135130540..3d3e2de7ac0918a00ef1fd40df97eeba0b404d2d 100644 (file)
@@ -57,7 +57,7 @@ public class ScriptedHealthCheck implements HealthCheck {
     public static final String HC_LABEL = "Health Check: Script";
     public static final String JCR_FILE_URL_PREFIX = "jcr:";
 
-    @ObjectClassDefinition(name = HC_LABEL, description = "Runs an arbitrary script in given scriping language (via javax.script). "
+    @ObjectClassDefinition(name = HC_LABEL, description = "Runs an arbitrary script in given scripting language (via javax.script). "
             + "The script has the following default bindings available: 'log', 'scriptHelper' and 'bundleContext'. "
             + "'log' is an instance of org.apache.felix.hc.api.FormattingResultLog and is used to define the result of the HC. "
             + "'scriptHelper.getService(classObj)' can be used as shortcut to retrieve a service."
@@ -75,7 +75,7 @@ public class ScriptedHealthCheck implements HealthCheck {
         @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.")
         String[] hc_tags() default {};
 
-        @AttributeDefinition(name = "Language", description = "The language the script is written in. To use e.g. 'groovy', ensure osgi bundle 'groovy-all' is available.")
+        @AttributeDefinition(name = "Language", description = "The language the script is written in. To use e.g. 'groovy', ensure osgi bundle 'groovy-jsr223' is available.")
         String language() default "groovy";
 
         @AttributeDefinition(name = "Script", description = "The script itself (either use 'script' or 'scriptUrl').")
index 3ebd9673017b2fd4b0d4e01154e0eb12f1d11482..aea2f4b0c1cdd84a206f0dc0c09657128052d025 100644 (file)
@@ -23,6 +23,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -45,7 +46,9 @@ import org.slf4j.LoggerFactory;
 public class ScriptEnginesTracker implements BundleListener {
     private static final Logger LOG = LoggerFactory.getLogger(ScriptEnginesTracker.class);
 
-    private static final String ENGINE_FACTORY_SERVICE = "META-INF/services/"  + ScriptEngineFactory.class.getName();
+    static final String META_INF_SERVICES = "META-INF/services";
+    static final String FACTORY_NAME = ScriptEngineFactory.class.getName();
+    private static final String ENGINE_FACTORY_SERVICE = String.format("%s/%s", META_INF_SERVICES, FACTORY_NAME);
     private final Map<String, ScriptEngineFactory> enginesByLanguage = new ConcurrentHashMap<String, ScriptEngineFactory>();
     private final Map<Bundle, List<String>> languagesByBundle = new ConcurrentHashMap<Bundle, List<String>>();
 
@@ -83,7 +86,7 @@ public class ScriptEnginesTracker implements BundleListener {
 
     
     public void bundleChanged(BundleEvent event) {
-        if (event.getType() == BundleEvent.STARTED && event.getBundle().getEntry(ENGINE_FACTORY_SERVICE) != null) {
+        if (event.getType() == BundleEvent.STARTED && event.getBundle().findEntries(META_INF_SERVICES, FACTORY_NAME, false) != null) {
             registerFactories(event.getBundle());
         } else if (event.getType() == BundleEvent.STOPPED) {
             unregisterFactories(event.getBundle());
@@ -94,7 +97,7 @@ public class ScriptEnginesTracker implements BundleListener {
     private void registerInitialScriptEngineFactories() {
         Bundle[] bundles = this.context.getBundles();
         for (Bundle bundle : bundles) {
-            if ( bundle.getState() == Bundle.ACTIVE  && bundle.getEntry(ENGINE_FACTORY_SERVICE)!=null) {
+            if ( bundle.getState() == Bundle.ACTIVE  && bundle.findEntries(META_INF_SERVICES, FACTORY_NAME, false) != null) {
                 registerFactories(bundle);
             }
         }
@@ -109,7 +112,7 @@ public class ScriptEnginesTracker implements BundleListener {
     }
 
     private void unregisterFactories(Bundle bundle) {
-        List<String> languagesForBundle = languagesByBundle.get(bundle);
+        List<String> languagesForBundle = languagesByBundle.remove(bundle);
         if(languagesForBundle != null) {
             for (String lang : languagesForBundle) {
                 ScriptEngineFactory removed = enginesByLanguage.remove(lang);
@@ -120,29 +123,30 @@ public class ScriptEnginesTracker implements BundleListener {
 
     @SuppressWarnings("unchecked")
     private List<ScriptEngineFactory> getScriptEngineFactoriesForBundle(final Bundle bundle) {
-        URL url = bundle.getEntry(ENGINE_FACTORY_SERVICE);
-        InputStream ins = null;
-        
         List<ScriptEngineFactory> scriptEngineFactoriesInBundle = new ArrayList<ScriptEngineFactory>();
-        
-        try {
-            ins = url.openStream();
-            BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
-            for (String className : getClassNames(reader)) {
-                try {
-                    Class<ScriptEngineFactory> clazz = (Class<ScriptEngineFactory>) bundle.loadClass(className);
-                    ScriptEngineFactory spi = clazz.newInstance();
-                    scriptEngineFactoriesInBundle.add(spi);
-                    
-                } catch (Throwable t) {
-                    LOG.error("Cannot register ScriptEngineFactory {}", className, t);
+        Enumeration<URL> foundEntries = bundle.findEntries(META_INF_SERVICES, FACTORY_NAME, false);
+        while (foundEntries != null && foundEntries.hasMoreElements()) {
+            URL url = foundEntries.nextElement();
+            InputStream ins = null;
+            try {
+                ins = url.openStream();
+                BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
+                for (String className : getClassNames(reader)) {
+                    try {
+                        Class<ScriptEngineFactory> clazz = (Class<ScriptEngineFactory>) bundle.loadClass(className);
+                        ScriptEngineFactory spi = clazz.getDeclaredConstructor().newInstance();
+                        scriptEngineFactoriesInBundle.add(spi);
+                        
+                    } catch (Throwable t) {
+                        LOG.error("Cannot register ScriptEngineFactory {}", className, t);
+                    }
                 }
-            }
 
-        } catch (IOException ioe) {
-            LOG.warn("Exception while trying to load factories as defined in {}", ENGINE_FACTORY_SERVICE, ioe);
-        } finally {
-            closeQuietly(ins);
+            } catch (IOException ioe) {
+                LOG.warn("Exception while trying to load factories as defined in {}", ENGINE_FACTORY_SERVICE, ioe);
+            } finally {
+                closeQuietly(ins);
+            }
         }
         
         return scriptEngineFactoriesInBundle;
diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/DummyScriptEngineFactory.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/DummyScriptEngineFactory.java
new file mode 100644 (file)
index 0000000..f04c555
--- /dev/null
@@ -0,0 +1,147 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.felix.hc.generalchecks.util;
+
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineFactory;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+
+/**
+ * A dummy script engine factory.
+ *
+ */
+public class DummyScriptEngineFactory implements ScriptEngineFactory {
+
+    class DummyScriptEngine implements ScriptEngine {
+
+        public Bindings createBindings() {
+            return new SimpleBindings();
+        }
+
+        public Object eval(String arg0) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object eval(Reader arg0) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object eval(String arg0, ScriptContext arg1) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object eval(Reader arg0, ScriptContext arg1) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object eval(String arg0, Bindings arg1) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object eval(Reader arg0, Bindings arg1) throws ScriptException {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object get(String arg0) {
+            throw new UnsupportedOperationException();
+        }
+
+        public Bindings getBindings(int arg0) {
+            throw new UnsupportedOperationException();
+        }
+
+        public ScriptContext getContext() {
+            throw new UnsupportedOperationException();
+        }
+
+        public ScriptEngineFactory getFactory() {
+            return DummyScriptEngineFactory.this;
+        }
+
+        public void put(String arg0, Object arg1) {
+            // NO-OP
+        }
+
+        public void setBindings(Bindings arg0, int arg1) {
+            // NO-OP
+        }
+
+        public void setContext(ScriptContext arg0) {
+            // NO-OP
+        }
+
+    }
+
+    public String getEngineName() {
+        return "Dummy Scripting Engine";
+    }
+
+    public String getEngineVersion() {
+        return "1.0";
+    }
+
+    public List<String> getExtensions() {
+        return Arrays.asList("dum", "dummy");
+    }
+
+    public String getLanguageName() {
+        return "dummy";
+    }
+
+    public String getLanguageVersion() {
+        return "2.0";
+    }
+
+    public String getMethodCallSyntax(String arg0, String arg1, String... arg2) {
+        throw new UnsupportedOperationException();
+    }
+
+    public List<String> getMimeTypes() {
+        return Collections.singletonList("application/x-dummy");
+    }
+
+    public List<String> getNames() {
+        return Arrays.asList("Dummy", "dummy");
+    }
+
+    public String getOutputStatement(String arg0) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Object getParameter(String arg0) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getProgram(String... arg0) {
+        throw new UnsupportedOperationException();
+    }
+
+    public ScriptEngine getScriptEngine() {
+        return new DummyScriptEngine();
+    }
+
+}
diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTrackerTest.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTrackerTest.java
new file mode 100644 (file)
index 0000000..522c995
--- /dev/null
@@ -0,0 +1,96 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.felix.hc.generalchecks.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+
+public class ScriptEnginesTrackerTest {
+
+    private static Class<?> SCRIPT_ENGINE_FACTORY = DummyScriptEngineFactory.class;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Test
+    public void testBundledScriptEngineFactory() throws Exception {
+        final URL url = createFactoryFile().toURI().toURL();
+        Bundle bundle = mock(Bundle.class);
+        when(bundle.getBundleId()).thenReturn(1L);
+        when(bundle.loadClass(SCRIPT_ENGINE_FACTORY.getName())).thenReturn((Class)SCRIPT_ENGINE_FACTORY);
+        when(bundle.findEntries(ScriptEnginesTracker.META_INF_SERVICES, ScriptEnginesTracker.FACTORY_NAME, false)).thenReturn(Collections.enumeration(Collections.singleton(url)));
+
+        // simulate a bundle starting that declares a new ScriptEngineFactory
+        BundleEvent bundleEvent = new BundleEvent(BundleEvent.STARTED, bundle);
+        ScriptEnginesTracker scriptEngineTracker = new ScriptEnginesTracker(); // context.getService(ScriptEnginesTracker.class);
+        assertNotNull("Expected that the ScriptEnginesTracker would already be registered.", scriptEngineTracker);
+        scriptEngineTracker.bundleChanged(bundleEvent);
+        Map<Bundle, List<String>> languagesByBundle = scriptEngineTracker.getLanguagesByBundle();
+        AtomicInteger factoriesSize = new AtomicInteger(0);
+        languagesByBundle.values().stream()
+            .forEach(l -> factoriesSize.addAndGet(l.size()));
+        int expectedScriptEngineFactories = 1;
+        assertEquals("Expected " + expectedScriptEngineFactories + " ScriptEngineFactories.", expectedScriptEngineFactories, factoriesSize.intValue());
+        List<String> factoriesForBundle = languagesByBundle.get(bundle);
+        assertEquals(1, factoriesForBundle.size());
+        assertEquals("dummy", factoriesForBundle.get(0));
+        assertEquals("Dummy Scripting Engine", scriptEngineTracker.getEngineByLanguage("dummy").getFactory().getEngineName());
+
+        // simulate a bundle stopping that previously declared a new ScriptEngineFactory
+        bundleEvent = new BundleEvent(BundleEvent.STOPPED, bundle);
+        scriptEngineTracker.bundleChanged(bundleEvent);
+        expectedScriptEngineFactories--;
+
+        factoriesSize.set(0);
+        languagesByBundle = scriptEngineTracker.getLanguagesByBundle();
+        assertNotNull(languagesByBundle);
+        languagesByBundle.values().stream()
+            .forEach(l -> factoriesSize.addAndGet(l.size()));
+        assertFalse(languagesByBundle.containsKey(bundle));
+        assertEquals("Expected " + expectedScriptEngineFactories + " ScriptEngineFactory.", expectedScriptEngineFactories, factoriesSize.intValue());
+        assertNull("Did not expect references to the already unregistered DummyScriptEngineFactory", 
+                scriptEngineTracker.getEngineByLanguage("dummy"));
+    }
+
+    private File createFactoryFile() throws IOException {
+        File tempFile = File.createTempFile("scriptEngine", "tmp");
+        tempFile.deleteOnExit();
+        try (FileOutputStream fos = new FileOutputStream(tempFile)) {
+            fos.write("#I'm a test-comment\n".getBytes());
+            fos.write(SCRIPT_ENGINE_FACTORY.getName().getBytes());
+        }
+        return tempFile;
+    }
+
+}