| /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| ~ 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.sling.scripting.core.impl.jsr223; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Dictionary; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| |
| import javax.script.ScriptEngine; |
| import javax.script.ScriptEngineFactory; |
| import javax.script.ScriptEngineManager; |
| |
| import org.apache.sling.api.scripting.SlingScriptConstants; |
| import org.apache.sling.commons.osgi.PropertiesUtil; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.BundleEvent; |
| import org.osgi.framework.BundleListener; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.ServiceReference; |
| import org.osgi.framework.wiring.BundleWiring; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.Deactivate; |
| import org.osgi.service.component.annotations.Reference; |
| import org.osgi.service.component.annotations.ReferenceCardinality; |
| import org.osgi.service.component.annotations.ReferencePolicy; |
| import org.osgi.service.component.annotations.ReferencePolicyOption; |
| import org.osgi.service.event.Event; |
| import org.osgi.service.event.EventAdmin; |
| import org.osgi.service.metatype.annotations.AttributeDefinition; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.osgi.service.metatype.annotations.ObjectClassDefinition; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @Component( |
| service = { |
| ScriptEngineManager.class, |
| SlingScriptEngineManager.class |
| }, |
| reference = @Reference( |
| name = "ScriptEngineFactory", |
| bind = "bindScriptEngineFactory", |
| unbind = "unbindScriptEngineFactory", |
| updated = "updatedScriptEngineFactory", |
| service = ScriptEngineFactory.class, |
| cardinality = ReferenceCardinality.MULTIPLE, |
| policy = ReferencePolicy.DYNAMIC |
| ) |
| ) |
| @Designate(ocd=SlingScriptEngineManager.Config.class) |
| public class SlingScriptEngineManager extends ScriptEngineManager implements BundleListener { |
| |
| @ObjectClassDefinition(name ="Apache Sling Script Engine Manager", |
| description="Configures options for the Script Engine Manager") |
| public @interface Config { |
| |
| @AttributeDefinition(name = "Includes", description = "A script engine with a short name that matches any of these expressions is included") |
| String[] includes() default {".*"}; |
| |
| @AttributeDefinition(name = "Excludes", description = "A script engine with a short name that matches any of these (optional) expressions is NOT included, even if it was accepted by the 'Includes' configuration") |
| String[] excludes(); |
| |
| } |
| |
| private ScriptEngineManager internalManager; |
| |
| private final Set<Bundle> engineSpiBundles = new HashSet<>(); |
| |
| private final Set<ServiceReference<ScriptEngineFactory>> serviceReferences = new HashSet<>(); |
| |
| private final SortedSet<SortableScriptEngineFactory> factories = new TreeSet<>(); |
| |
| private BundleContext bundleContext; |
| |
| @Reference( |
| policy = ReferencePolicy.DYNAMIC, |
| policyOption = ReferencePolicyOption.GREEDY, |
| cardinality = ReferenceCardinality.OPTIONAL |
| ) |
| private volatile EventAdmin eventAdmin; |
| |
| static final String EVENT_TOPIC_SCRIPT_MANAGER_UPDATED = "org/apache/sling/scripting/core/impl/jsr223/SlingScriptEngineManager/UPDATED"; |
| |
| static final String META_INF_SERVICES = "META-INF/services"; |
| static final String FACTORY_NAME = ScriptEngineFactory.class.getName(); |
| |
| private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); |
| |
| private final Logger logger = LoggerFactory.getLogger(SlingScriptEngineManager.class); |
| |
| private Set<Pattern> includePatterns = Collections.emptySet(); |
| private Set<Pattern> excludePatterns = Collections.emptySet(); |
| |
| @Override |
| public ScriptEngine getEngineByName(String shortName) { |
| readWriteLock.readLock().lock(); |
| try { |
| SortableScriptEngineFactory ssef = factories.stream() |
| .filter(factory -> factory.getNames().contains(shortName)) |
| .findFirst() |
| .orElse(null); |
| return ssef == null ? null : ssef.getScriptEngine(); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| public List<ScriptEngine> getEnginesByName(final String shortName) { |
| readWriteLock.readLock().lock(); |
| try { |
| return factories.stream() |
| // first, check exact match of short names |
| .filter(factory -> factory.getNames().contains(shortName) || |
| // then, check contains match of long name |
| // for backward compatibility |
| factory.getEngineName().contains(shortName)) |
| .map(factory -> factory.getDelegate().getScriptEngine()) |
| .collect(Collectors.toList()); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public ScriptEngine getEngineByExtension(String extension) { |
| readWriteLock.readLock().lock(); |
| try { |
| SortableScriptEngineFactory ssef = factories.stream() |
| .filter(factory -> factory.getExtensions().contains(extension)) |
| .findFirst() |
| .orElse(null); |
| return ssef == null ? null : ssef.getScriptEngine(); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| public List<ScriptEngine> getEnginesByExtension(final String extension) { |
| readWriteLock.readLock().lock(); |
| try { |
| return factories.stream() |
| .filter(factory -> factory.getExtensions().contains(extension)) |
| .map(factory -> factory.getDelegate().getScriptEngine()) |
| .collect(Collectors.toList()); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public ScriptEngine getEngineByMimeType(String mimeType) { |
| readWriteLock.readLock().lock(); |
| try { |
| SortableScriptEngineFactory ssef = factories.stream() |
| .filter(factory -> factory.getMimeTypes().contains(mimeType)) |
| .findFirst() |
| .orElse(null); |
| return ssef == null ? null : ssef.getScriptEngine(); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| public List<ScriptEngine> getEnginesByMimeType(final String mimeType) { |
| readWriteLock.readLock().lock(); |
| try { |
| return factories.stream() |
| .filter(factory -> factory.getMimeTypes().contains(mimeType)) |
| .map(factory -> factory.getDelegate().getScriptEngine()) |
| .collect(Collectors.toList()); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public List<ScriptEngineFactory> getEngineFactories() { |
| readWriteLock.readLock().lock(); |
| try { |
| ArrayList<ScriptEngineFactory> list = new ArrayList<>(factories.size()); |
| list.addAll(factories); |
| return Collections.unmodifiableList(list); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void registerEngineName(String name, ScriptEngineFactory factory) { |
| readWriteLock.writeLock().lock(); |
| try { |
| internalManager.registerEngineName(name, factory); |
| } finally { |
| readWriteLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void registerEngineMimeType(String type, ScriptEngineFactory factory) { |
| readWriteLock.writeLock().lock(); |
| try { |
| internalManager.registerEngineMimeType(type, factory); |
| } finally { |
| readWriteLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void registerEngineExtension(String extension, ScriptEngineFactory factory) { |
| readWriteLock.writeLock().lock(); |
| try { |
| internalManager.registerEngineExtension(extension, factory); |
| } finally { |
| readWriteLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void bundleChanged(BundleEvent event) { |
| if (event.getType() == BundleEvent.STARTED |
| && event.getBundle().getBundleId() > 0 |
| // SLING-11398 - use findEntries instead of getEntry to support fragments |
| && event.getBundle().findEntries(META_INF_SERVICES, FACTORY_NAME, false) != null) { |
| synchronized (this.engineSpiBundles) { |
| this.engineSpiBundles.add(event.getBundle()); |
| } |
| updateFactories(); |
| } else if (event.getType() == BundleEvent.STOPPED) { |
| boolean refresh; |
| synchronized (this.engineSpiBundles) { |
| refresh = this.engineSpiBundles.remove(event.getBundle()); |
| } |
| if (refresh) { |
| updateFactories(); |
| } |
| } |
| } |
| |
| public Map<String, Object> getServiceProperties(final ScriptEngineFactory factory) { |
| readWriteLock.readLock().lock(); |
| try { |
| return factories.stream().filter(f -> f.getDelegate().equals(factory)).findFirst().map(SortableScriptEngineFactory::getServiceProperties).orElse(null); |
| } finally { |
| readWriteLock.readLock().unlock(); |
| } |
| } |
| |
| @Activate |
| private void activate(final Config config, final BundleContext bundleContext) { |
| String[] includes = config.includes(); |
| if (includes == null) { |
| this.includePatterns = Collections.emptySet(); |
| } else { |
| this.includePatterns = new HashSet<>(); |
| for (String pattern : includes) { |
| if (!pattern.isEmpty()) { |
| Pattern p = Pattern.compile(pattern); |
| includePatterns.add(p); |
| } |
| } |
| } |
| |
| String[] excludes = config.excludes(); |
| if (excludes == null) { |
| this.excludePatterns = Collections.emptySet(); |
| } else { |
| this.excludePatterns = new HashSet<>(); |
| for (String pattern : excludes) { |
| if (!pattern.isEmpty()) { |
| Pattern p = Pattern.compile(pattern); |
| excludePatterns.add(p); |
| } |
| } |
| } |
| |
| this.bundleContext = bundleContext; |
| bundleContext.addBundleListener(this); |
| registerInitialScriptEngineFactories(); |
| } |
| |
| /** |
| * Handles any spi bundles that were already started before we started listening |
| */ |
| private void registerInitialScriptEngineFactories() { |
| Bundle[] bundles = this.bundleContext.getBundles(); |
| for (Bundle bundle : bundles) { |
| if (bundle.getState() == Bundle.ACTIVE |
| && bundle.getBundleId() > 0 |
| && bundle.findEntries(META_INF_SERVICES, FACTORY_NAME, false) != null) { |
| synchronized (this.engineSpiBundles) { |
| this.engineSpiBundles.add(bundle); |
| } |
| } |
| } |
| updateFactories(); |
| } |
| |
| /** |
| * Check if the given factory matches any of the include/excludes patterns |
| * |
| * @param sef the factory to check |
| * @return true if included, false otherwise |
| */ |
| private boolean isIncluded(ScriptEngineFactory sef) { |
| boolean include = false; |
| |
| if (!this.includePatterns.isEmpty()) { |
| List<String> names = sef.getNames(); |
| for (String name : names) { |
| for (Pattern p : this.includePatterns) { |
| if (p.matcher(name).matches()) { |
| include = true; |
| if (logger.isDebugEnabled()) { |
| logger.debug("ScriptEngineFactory \"{}\" matches the include pattern \"{}\" for name \"{}\"", sef.getEngineName(), p.pattern(), name); |
| } |
| break; // found a match so stop looking further |
| } |
| } |
| if (include) { |
| break; // break out of the outer loop too |
| } |
| } |
| } |
| |
| if (include && !this.excludePatterns.isEmpty()) { |
| List<String> names = sef.getNames(); |
| for (String name : names) { |
| for (Pattern p : this.excludePatterns) { |
| if (p.matcher(name).matches()) { |
| include = false; |
| if (logger.isDebugEnabled()) { |
| logger.debug("ScriptEngineFactory \"{}\" matches the exclude pattern \"{}\" for name \"{}\" so it is not included", sef.getEngineName(), p.pattern(), name); |
| } |
| break; // found a match so stop looking further |
| } |
| } |
| if (!include) { |
| break; // break out of the outer loop too |
| } |
| } |
| } |
| return include; |
| } |
| |
| @Deactivate |
| private void deactivate(final BundleContext bundleContext) { |
| bundleContext.removeBundleListener(this); |
| } |
| |
| @SuppressWarnings("unused") |
| private void bindScriptEngineFactory(final ServiceReference<ScriptEngineFactory> serviceReference, final ScriptEngineFactory factory) { |
| synchronized (this.serviceReferences) { |
| serviceReferences.add(serviceReference); |
| } |
| updateFactories(); |
| postEvent(SlingScriptConstants.TOPIC_SCRIPT_ENGINE_FACTORY_ADDED, factory); |
| } |
| |
| @SuppressWarnings("unused") |
| private void unbindScriptEngineFactory(final ServiceReference<ScriptEngineFactory> serviceReference, final ScriptEngineFactory factory) { |
| synchronized (this.serviceReferences) { |
| serviceReferences.remove(serviceReference); |
| if (bundleContext != null) { |
| bundleContext.ungetService(serviceReference); |
| } |
| } |
| updateFactories(); |
| postEvent(SlingScriptConstants.TOPIC_SCRIPT_ENGINE_FACTORY_REMOVED, factory); |
| } |
| |
| @SuppressWarnings("unused") |
| private void updatedScriptEngineFactory(final ServiceReference<ScriptEngineFactory> serviceReference, final ScriptEngineFactory factory) { |
| updateFactories(); |
| postEvent(SlingScriptConstants.TOPIC_SCRIPT_ENGINE_FACTORY_UPDATED, factory); |
| } |
| |
| private void updateFactories() { |
| readWriteLock.writeLock().lock(); |
| try { |
| internalManager = getInternalScriptEngineManager(); |
| factories.clear(); |
| |
| long fakeBundleIdCounter = Long.MIN_VALUE; |
| // first add the platform factories |
| for (final ScriptEngineFactory factory : internalManager.getEngineFactories()) { |
| if (isIncluded(factory)) { |
| final SortableScriptEngineFactory sortableScriptEngineFactory = new SortableScriptEngineFactory(factory, fakeBundleIdCounter++, 0, null); |
| factories.add(sortableScriptEngineFactory); |
| } |
| } |
| |
| // then factories from SPI Bundles |
| final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| try { |
| Thread.currentThread().setContextClassLoader(null); |
| for (final Bundle bundle : engineSpiBundles) { |
| try { |
| final ScriptEngineManager manager = new ScriptEngineManager(bundle.adapt(BundleWiring.class).getClassLoader()); |
| for (final ScriptEngineFactory factory : manager.getEngineFactories()) { |
| if (isIncluded(factory)) { |
| final SortableScriptEngineFactory sortableScriptEngineFactory = new SortableScriptEngineFactory(factory, bundle.getBundleId(), 0, null); |
| factories.add(sortableScriptEngineFactory); |
| } |
| } |
| } catch (Exception ex) { |
| logger.error("Unable to process bundle " + bundle.getSymbolicName(), ex); |
| } |
| } |
| } |
| finally { |
| Thread.currentThread().setContextClassLoader(loader); |
| } |
| // and finally factories registered as OSGi services |
| if (bundleContext != null) { |
| synchronized (this.serviceReferences) { |
| for (final ServiceReference<ScriptEngineFactory> serviceReference : serviceReferences) { |
| final ScriptEngineFactory scriptEngineFactory = bundleContext.getService(serviceReference); |
| if (isIncluded(scriptEngineFactory)) { |
| final Map<String, Object> factoryProperties = new HashMap<>(serviceReference.getPropertyKeys().length); |
| for (final String key : serviceReference.getPropertyKeys()) { |
| factoryProperties.put(key, serviceReference.getProperty(key)); |
| } |
| final SortableScriptEngineFactory sortableScriptEngineFactory = new SortableScriptEngineFactory(scriptEngineFactory, serviceReference.getBundle().getBundleId(), PropertiesUtil.toInteger(serviceReference.getProperty(Constants.SERVICE_RANKING), 0), factoryProperties); |
| factories.add(sortableScriptEngineFactory); |
| } |
| } |
| } |
| } |
| // register the associations at the end, so that the priority sorting is taken into consideration |
| for (final ScriptEngineFactory factory : factories) { |
| registerAssociations(factory); |
| } |
| if (eventAdmin != null) { |
| eventAdmin.postEvent(new Event(EVENT_TOPIC_SCRIPT_MANAGER_UPDATED, Collections.emptyMap())); |
| } |
| } finally { |
| readWriteLock.writeLock().unlock(); |
| } |
| } |
| |
| private ScriptEngineManager getInternalScriptEngineManager() { |
| final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| try { |
| Thread.currentThread().setContextClassLoader(null); |
| return new ScriptEngineManager(ClassLoader.getSystemClassLoader()); |
| } |
| finally { |
| Thread.currentThread().setContextClassLoader(loader); |
| } |
| } |
| |
| private void registerAssociations(ScriptEngineFactory factory) { |
| for (String extension : factory.getExtensions()) { |
| if (extension != null && !extension.isEmpty()) { |
| internalManager.registerEngineExtension(extension, factory); |
| } else { |
| logger.warn("Could not register an empty or null extension for script engine factory {}.", factory.getEngineName()); |
| } |
| } |
| for (String mimeType : factory.getMimeTypes()) { |
| if (mimeType != null && !mimeType.isEmpty()) { |
| internalManager.registerEngineMimeType(mimeType, factory); |
| } else { |
| logger.warn("Could not register an empty or null mime type for script engine factory {}.", factory.getEngineName()); |
| } |
| } |
| for (String name : factory.getNames()) { |
| if (name != null && !name.isEmpty()) { |
| internalManager.registerEngineName(name, factory); |
| } else { |
| logger.warn("Could not register an empty or null engine name for script engine factory {}.", factory.getEngineName()); |
| } |
| } |
| } |
| |
| private void postEvent(final String topic, final ScriptEngineFactory scriptEngineFactory) { |
| if (eventAdmin != null) { |
| final Dictionary<String, Object> props = new Hashtable<>(); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_NAME, scriptEngineFactory.getEngineName()); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_VERSION, scriptEngineFactory.getEngineVersion()); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_EXTENSIONS, scriptEngineFactory.getExtensions().toArray(new String[0])); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_LANGUAGE_NAME, scriptEngineFactory.getLanguageName()); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_LANGUAGE_VERSION, scriptEngineFactory.getLanguageVersion()); |
| props.put(SlingScriptConstants.PROPERTY_SCRIPT_ENGINE_FACTORY_MIME_TYPES, scriptEngineFactory.getMimeTypes().toArray(new String[0])); |
| eventAdmin.postEvent(new Event(topic, props)); |
| } |
| } |
| |
| } |