blob: 63db942318679799deef6c62697bb00a195654a1 [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ 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));
}
}
}