blob: 579e11fd7ac07736ad816a3a6dc7982f5e90e0e5 [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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.script.Compilable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.commons.threads.ThreadPool;
import org.apache.sling.commons.threads.ThreadPoolManager;
import org.apache.sling.scripting.api.CachedScript;
import org.apache.sling.scripting.api.ScriptCache;
import org.apache.sling.scripting.core.impl.helper.CachingMap;
import org.apache.sling.scripting.core.impl.jsr223.SlingScriptEngineManager;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
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.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = {ScriptCache.class, EventHandler.class},
property = {
Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
EventConstants.EVENT_TOPIC + "=org/apache/sling/scripting/core/impl/jsr223/SlingScriptEngineManager/*"
}
)
@Designate(
ocd = ScriptCacheImplConfiguration.class
)
/**
* The {@code ScriptCache} stores information about {@link CompiledScript} instances evaluated by various {@link ScriptEngine}s that
* implement the {@link Compilable} interface.
*/
public class ScriptCacheImpl implements ScriptCache, ResourceChangeListener, ExternalResourceChangeListener, EventHandler {
private final Logger logger = LoggerFactory.getLogger(ScriptCacheImpl.class);
public static final int DEFAULT_CACHE_SIZE = 65536;
private BundleContext bundleContext;
private Map<String, SoftReference<CachedScript>> internalMap;
private ServiceRegistration<ResourceChangeListener> resourceChangeListener;
private Set<String> extensions = new HashSet<>();
private String[] additionalExtensions = new String[]{};
// use a static policy so that we can reconfigure the watched script files if the search paths are changed
@Reference
private ResourceResolverFactory rrf;
@Reference
private ThreadPoolManager threadPoolManager;
@Reference
private SlingScriptEngineManager slingScriptEngineManager;
private ThreadPool threadPool;
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
private volatile boolean active = false;
@Reference
private ServiceUserMapped serviceUserMapped;
public ScriptCacheImpl() {
internalMap = new CachingMap<>(DEFAULT_CACHE_SIZE);
}
@Override
public CachedScript getScript(String scriptPath) {
readLock.lock();
SoftReference<CachedScript> reference = null;
try {
reference = internalMap.get(scriptPath);
} finally {
readLock.unlock();
}
return reference != null ? reference.get() : null;
}
@Override
public void putScript(CachedScript script) {
writeLock.lock();
try {
SoftReference<CachedScript> reference = new SoftReference<>(script);
internalMap.put(script.getScriptPath(), reference);
logger.debug("Added script {} to script cache.", script.getScriptPath());
} finally {
writeLock.unlock();
}
}
@Override
public void clear() {
writeLock.lock();
try {
internalMap.clear();
logger.debug("Cleared script cache.");
} finally {
writeLock.unlock();
}
}
@Override
public boolean removeScript(String scriptPath) {
writeLock.lock();
try {
SoftReference<CachedScript> reference = internalMap.remove(scriptPath);
boolean result = reference != null;
if (result) {
logger.debug("Removed script {} from script cache.", scriptPath);
}
return result;
} finally {
writeLock.unlock();
}
}
@Override
public void onChange(@NotNull List<ResourceChange> list) {
for (final ResourceChange change : list) {
Runnable eventTask = () -> {
String path = change.getPath();
writeLock.lock();
try {
final boolean removed = internalMap.remove(path) != null;
logger.debug("Detected script change for {} - removed entry from the cache.", path);
if ( !removed && change.getType() == ChangeType.REMOVED ) {
final String prefix = path + "/";
final Set<String> removal = new HashSet<>();
for(final Map.Entry<String, SoftReference<CachedScript>> entry : internalMap.entrySet()) {
if ( entry.getKey().startsWith(prefix) ) {
removal.add(entry.getKey());
}
}
for(final String key : removal) {
internalMap.remove(key);
logger.debug("Detected removal for {} - removed entry {} from the cache.", path, key);
}
}
} finally {
writeLock.unlock();
}
};
threadPool.execute(eventTask);
}
}
protected Set<String> getCachedScripts() {
readLock.lock();
try {
return internalMap.keySet();
} finally {
readLock.unlock();
}
}
@Activate
protected void activate(ScriptCacheImplConfiguration configuration, BundleContext bundleCtx) {
threadPool = threadPoolManager.get("Script Cache Thread Pool");
bundleContext = bundleCtx;
additionalExtensions = configuration.org_apache_sling_scripting_cache_additional__extensions();
int newMaxCacheSize = configuration.org_apache_sling_scripting_cache_size();
if (newMaxCacheSize != DEFAULT_CACHE_SIZE) {
// change the map only if there's a configuration change regarding the cache's max size
CachingMap<CachedScript> newMap = new CachingMap<>(newMaxCacheSize);
newMap.putAll(internalMap);
internalMap = newMap;
}
active = true;
configureCache();
}
private void configureCache() {
writeLock.lock();
try {
if (active) {
if (resourceChangeListener != null) {
resourceChangeListener.unregister();
resourceChangeListener = null;
}
internalMap.clear();
if (additionalExtensions != null) {
extensions.addAll(Arrays.asList(additionalExtensions));
}
if (!extensions.isEmpty()) {
Set<String> globPatterns = new HashSet<>(extensions.size());
for (String extension : extensions) {
globPatterns.add("glob:**/*." + extension);
}
Dictionary<String, Object> resourceChangeListenerProperties = new Hashtable<>(); // NOSONAR
resourceChangeListenerProperties
.put(ResourceChangeListener.PATHS, globPatterns.toArray(new String[globPatterns.size()]));
resourceChangeListenerProperties.put(ResourceChangeListener.CHANGES,
new String[]{ResourceChange.ChangeType.CHANGED.name(), ResourceChange.ChangeType.REMOVED.name()});
resourceChangeListener =
bundleContext.registerService(
ResourceChangeListener.class,
this,
resourceChangeListenerProperties
);
}
}
} finally {
writeLock.unlock();
}
}
@Deactivate
protected void deactivate() {
writeLock.lock();
try {
internalMap.clear();
if (resourceChangeListener != null) {
resourceChangeListener.unregister();
resourceChangeListener = null;
}
if (threadPool != null) {
threadPoolManager.release(threadPool);
threadPool = null;
}
active = false;
} finally {
writeLock.unlock();
}
}
@Override
public void handleEvent(Event event) {
clear();
extensions.clear();
for (ScriptEngineFactory factory : slingScriptEngineManager.getEngineFactories()) {
ScriptEngine scriptEngine = factory.getScriptEngine();
if (scriptEngine instanceof Compilable) {
extensions.addAll(factory.getExtensions());
}
}
configureCache();
}
}