/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.script; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.apache.lucene.util.IOUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptResponse; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.PutStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.PutStoredScriptResponse; import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.breaker.CircuitBreakingException; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.cache.RemovalListener; import org.elasticsearch.common.cache.RemovalNotification; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.env.Environment; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.template.CompiledTemplate; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import java.io.Closeable; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentMap; import static java.util.Collections.unmodifiableMap; public class ScriptService extends AbstractComponent implements Closeable, ClusterStateListener { static final String DISABLE_DYNAMIC_SCRIPTING_SETTING = "script.disable_dynamic"; public static final Setting<Integer> SCRIPT_CACHE_SIZE_SETTING = Setting.intSetting("script.cache.max_size", 100, 0, Property.NodeScope); public static final Setting<TimeValue> SCRIPT_CACHE_EXPIRE_SETTING = Setting.positiveTimeSetting("script.cache.expire", TimeValue.timeValueMillis(0), Property.NodeScope); public static final Setting<Boolean> SCRIPT_AUTO_RELOAD_ENABLED_SETTING = Setting.boolSetting("script.auto_reload_enabled", true, Property.NodeScope, Property.Deprecated); public static final Setting<Integer> SCRIPT_MAX_SIZE_IN_BYTES = Setting.intSetting("script.max_size_in_bytes", 65535, Property.NodeScope); public static final Setting<Integer> SCRIPT_MAX_COMPILATIONS_PER_MINUTE = Setting.intSetting("script.max_compilations_per_minute", 15, 0, Property.Dynamic, Property.NodeScope); private final Collection<ScriptEngine> scriptEngines; private final Map<String, ScriptEngine> scriptEnginesByLang; private final Map<String, ScriptEngine> scriptEnginesByExt; private final ConcurrentMap<CacheKey, CompiledScript> staticCache = ConcurrentCollections.newConcurrentMap(); private final Cache<CacheKey, CompiledScript> cache; private final Path scriptsDirectory; private final ScriptModes scriptModes; private final ScriptContextRegistry scriptContextRegistry; private final ScriptMetrics scriptMetrics = new ScriptMetrics(); private ClusterState clusterState; private int totalCompilesPerMinute; private long lastInlineCompileTime; private double scriptsPerMinCounter; private double compilesAllowedPerNano; public ScriptService(Settings settings, Environment env, ResourceWatcherService resourceWatcherService, ScriptEngineRegistry scriptEngineRegistry, ScriptContextRegistry scriptContextRegistry, ScriptSettings scriptSettings) throws IOException { super(settings); Objects.requireNonNull(scriptEngineRegistry); Objects.requireNonNull(scriptContextRegistry); Objects.requireNonNull(scriptSettings); if (Strings.hasLength(settings.get(DISABLE_DYNAMIC_SCRIPTING_SETTING))) { throw new IllegalArgumentException(DISABLE_DYNAMIC_SCRIPTING_SETTING + " is not a supported setting, replace with fine-grained script settings. \n" + "Dynamic scripts can be enabled for all languages and all operations by replacing `script.disable_dynamic: false` with `script.inline: true` and `script.stored: true` in elasticsearch.yml"); } this.scriptEngines = scriptEngineRegistry.getRegisteredLanguages().values(); this.scriptContextRegistry = scriptContextRegistry; int cacheMaxSize = SCRIPT_CACHE_SIZE_SETTING.get(settings); CacheBuilder<CacheKey, CompiledScript> cacheBuilder = CacheBuilder.builder(); if (cacheMaxSize >= 0) { cacheBuilder.setMaximumWeight(cacheMaxSize); } TimeValue cacheExpire = SCRIPT_CACHE_EXPIRE_SETTING.get(settings); if (cacheExpire.getNanos() != 0) { cacheBuilder.setExpireAfterAccess(cacheExpire); } logger.debug("using script cache with max_size [{}], expire [{}]", cacheMaxSize, cacheExpire); this.cache = cacheBuilder.removalListener(new ScriptCacheRemovalListener()).build(); Map<String, ScriptEngine> enginesByLangBuilder = new HashMap<>(); Map<String, ScriptEngine> enginesByExtBuilder = new HashMap<>(); for (ScriptEngine scriptEngine : scriptEngines) { String language = scriptEngineRegistry.getLanguage(scriptEngine.getClass()); enginesByLangBuilder.put(language, scriptEngine); enginesByExtBuilder.put(scriptEngine.getExtension(), scriptEngine); } this.scriptEnginesByLang = unmodifiableMap(enginesByLangBuilder); this.scriptEnginesByExt = unmodifiableMap(enginesByExtBuilder); this.scriptModes = new ScriptModes(scriptSettings, settings); // add file watcher for static scripts scriptsDirectory = env.scriptsFile(); if (logger.isTraceEnabled()) { logger.trace("Using scripts directory [{}] ", scriptsDirectory); } FileWatcher fileWatcher = new FileWatcher(scriptsDirectory); fileWatcher.addListener(new ScriptChangesListener()); if (SCRIPT_AUTO_RELOAD_ENABLED_SETTING.get(settings) && resourceWatcherService != null) { // automatic reload is enabled - register scripts resourceWatcherService.add(fileWatcher); } else { // automatic reload is disable just load scripts once fileWatcher.init(); } this.lastInlineCompileTime = System.nanoTime(); this.setMaxCompilationsPerMinute(SCRIPT_MAX_COMPILATIONS_PER_MINUTE.get(settings)); } void registerClusterSettingsListeners(ClusterSettings clusterSettings) { clusterSettings.addSettingsUpdateConsumer(SCRIPT_MAX_COMPILATIONS_PER_MINUTE, this::setMaxCompilationsPerMinute); } @Override public void close() throws IOException { IOUtils.close(scriptEngines); } private ScriptEngine getScriptEngineServiceForLang(String lang) { ScriptEngine scriptEngine = scriptEnginesByLang.get(lang); if (scriptEngine == null) { throw new IllegalArgumentException("script_lang not supported [" + lang + "]"); } return scriptEngine; } private ScriptEngine getScriptEngineServiceForFileExt(String fileExtension) { ScriptEngine scriptEngine = scriptEnginesByExt.get(fileExtension); if (scriptEngine == null) { throw new IllegalArgumentException("script file extension not supported [" + fileExtension + "]"); } return scriptEngine; } void setMaxCompilationsPerMinute(Integer newMaxPerMinute) { this.totalCompilesPerMinute = newMaxPerMinute; // Reset the counter to allow new compilations this.scriptsPerMinCounter = totalCompilesPerMinute; this.compilesAllowedPerNano = ((double) totalCompilesPerMinute) / TimeValue.timeValueMinutes(1).nanos(); } /** * Checks if a script can be executed and compiles it if needed, or returns the previously compiled and cached script. */ public CompiledScript compile(Script script, ScriptContext scriptContext) { Objects.requireNonNull(script); Objects.requireNonNull(scriptContext); ScriptType type = script.getType(); String lang = script.getLang(); String idOrCode = script.getIdOrCode(); Map<String, String> options = script.getOptions(); String id = idOrCode; // lang may be null when looking up a stored script, so we must get the // source to retrieve the lang before checking if the context is supported if (type == ScriptType.STORED) { // search template requests can possibly pass in the entire path instead // of just an id for looking up a stored script, so we parse the path and // check for appropriate errors String[] path = id.split("/"); if (path.length == 3) { if (lang != null && lang.equals(path[1]) == false) { throw new IllegalStateException("conflicting script languages, found [" + path[1] + "] but expected [" + lang + "]"); } id = path[2]; deprecationLogger.deprecated("use of </lang/id> [" + idOrCode + "] for looking up" + " stored scripts/templates has been deprecated, use only <id> [" + id + "] instead"); } else if (path.length != 1) { throw new IllegalArgumentException("illegal stored script format [" + id + "] use only <id>"); } // a stored script must be pulled from the cluster state every time in case // the script has been updated since the last compilation StoredScriptSource source = getScriptFromClusterState(id, lang); lang = source.getLang(); idOrCode = source.getCode(); options = source.getOptions(); } // TODO: fix this through some API or something, that's wrong // special exception to prevent expressions from compiling as update or mapping scripts boolean expression = "expression".equals(script.getLang()); boolean notSupported = scriptContext.getKey().equals(ScriptContext.Standard.UPDATE.getKey()); if (expression && notSupported) { throw new UnsupportedOperationException("scripts of type [" + script.getType() + "]," + " operation [" + scriptContext.getKey() + "] and lang [" + lang + "] are not supported"); } ScriptEngine scriptEngine = getScriptEngineServiceForLang(lang); if (canExecuteScript(lang, type, scriptContext) == false) { throw new IllegalStateException("scripts of type [" + script.getType() + "]," + " operation [" + scriptContext.getKey() + "] and lang [" + lang + "] are disabled"); } if (logger.isTraceEnabled()) { logger.trace("compiling lang: [{}] type: [{}] script: {}", lang, type, idOrCode); } if (type == ScriptType.FILE) { CacheKey cacheKey = new CacheKey(lang, idOrCode, options); CompiledScript compiledScript = staticCache.get(cacheKey); if (compiledScript == null) { throw new IllegalArgumentException("unable to find file script [" + idOrCode + "] using lang [" + lang + "]"); } return compiledScript; } CacheKey cacheKey = new CacheKey(lang, idOrCode, options); CompiledScript compiledScript = cache.get(cacheKey); if (compiledScript != null) { return compiledScript; } // Synchronize so we don't compile scripts many times during multiple shards all compiling a script synchronized (this) { // Retrieve it again in case it has been put by a different thread compiledScript = cache.get(cacheKey); if (compiledScript == null) { try { // Either an un-cached inline script or indexed script // If the script type is inline the name will be the same as the code for identification in exceptions // but give the script engine the chance to be better, give it separate name + source code // for the inline case, then its anonymous: null. if (logger.isTraceEnabled()) { logger.trace("compiling script, type: [{}], lang: [{}], options: [{}]", type, lang, options); } // Check whether too many compilations have happened checkCompilationLimit(); compiledScript = new CompiledScript(type, id, lang, scriptEngine.compile(id, idOrCode, options)); } catch (ScriptException good) { // TODO: remove this try-catch completely, when all script engines have good exceptions! throw good; // its already good } catch (Exception exception) { throw new GeneralScriptException("Failed to compile " + type + " script [" + id + "] using lang [" + lang + "]", exception); } // Since the cache key is the script content itself we don't need to // invalidate/check the cache if an indexed script changes. scriptMetrics.onCompilation(); cache.put(cacheKey, compiledScript); } return compiledScript; } } /** Compiles a template. Note this will be moved to a separate TemplateService in the future. */ public CompiledTemplate compileTemplate(Script script, ScriptContext scriptContext) { CompiledScript compiledScript = compile(script, scriptContext); return params -> (BytesReference)executable(compiledScript, params).run(); } /** * Check whether there have been too many compilations within the last minute, throwing a circuit breaking exception if so. * This is a variant of the token bucket algorithm: https://en.wikipedia.org/wiki/Token_bucket * * It can be thought of as a bucket with water, every time the bucket is checked, water is added proportional to the amount of time that * elapsed since the last time it was checked. If there is enough water, some is removed and the request is allowed. If there is not * enough water the request is denied. Just like a normal bucket, if water is added that overflows the bucket, the extra water/capacity * is discarded - there can never be more water in the bucket than the size of the bucket. */ void checkCompilationLimit() { long now = System.nanoTime(); long timePassed = now - lastInlineCompileTime; lastInlineCompileTime = now; scriptsPerMinCounter += (timePassed) * compilesAllowedPerNano; // It's been over the time limit anyway, readjust the bucket to be level if (scriptsPerMinCounter > totalCompilesPerMinute) { scriptsPerMinCounter = totalCompilesPerMinute; } // If there is enough tokens in the bucket, allow the request and decrease the tokens by 1 if (scriptsPerMinCounter >= 1) { scriptsPerMinCounter -= 1.0; } else { // Otherwise reject the request throw new CircuitBreakingException("[script] Too many dynamic script compilations within one minute, max: [" + totalCompilesPerMinute + "/min]; please use on-disk, indexed, or scripts with parameters instead; " + "this limit can be changed by the [" + SCRIPT_MAX_COMPILATIONS_PER_MINUTE.getKey() + "] setting"); } } public boolean isLangSupported(String lang) { Objects.requireNonNull(lang); return scriptEnginesByLang.containsKey(lang); } StoredScriptSource getScriptFromClusterState(String id, String lang) { if (lang != null && isLangSupported(lang) == false) { throw new IllegalArgumentException("unable to get stored script with unsupported lang [" + lang + "]"); } ScriptMetaData scriptMetadata = clusterState.metaData().custom(ScriptMetaData.TYPE); if (scriptMetadata == null) { throw new ResourceNotFoundException("unable to find script [" + id + "]" + (lang == null ? "" : " using lang [" + lang + "]") + " in cluster state"); } StoredScriptSource source = scriptMetadata.getStoredScript(id, lang); if (source == null) { throw new ResourceNotFoundException("unable to find script [" + id + "]" + (lang == null ? "" : " using lang [" + lang + "]") + " in cluster state"); } return source; } public void putStoredScript(ClusterService clusterService, PutStoredScriptRequest request, ActionListener<PutStoredScriptResponse> listener) { int max = SCRIPT_MAX_SIZE_IN_BYTES.get(settings); if (request.content().length() > max) { throw new IllegalArgumentException("exceeded max allowed stored script size in bytes [" + max + "] with size [" + request.content().length() + "] for script [" + request.id() + "]"); } StoredScriptSource source = StoredScriptSource.parse(request.lang(), request.content(), request.xContentType()); if (isLangSupported(source.getLang()) == false) { throw new IllegalArgumentException("unable to put stored script with unsupported lang [" + source.getLang() + "]"); } try { ScriptEngine scriptEngine = getScriptEngineServiceForLang(source.getLang()); if (isAnyScriptContextEnabled(source.getLang(), ScriptType.STORED)) { Object compiled = scriptEngine.compile(request.id(), source.getCode(), Collections.emptyMap()); if (compiled == null) { throw new IllegalArgumentException("failed to parse/compile stored script [" + request.id() + "]" + (source.getCode() == null ? "" : " using code [" + source.getCode() + "]")); } } else { throw new IllegalArgumentException( "cannot put stored script [" + request.id() + "], stored scripts cannot be run under any context"); } } catch (ScriptException good) { throw good; } catch (Exception exception) { throw new IllegalArgumentException("failed to parse/compile stored script [" + request.id() + "]", exception); } clusterService.submitStateUpdateTask("put-script-" + request.id(), new AckedClusterStateUpdateTask<PutStoredScriptResponse>(request, listener) { @Override protected PutStoredScriptResponse newResponse(boolean acknowledged) { return new PutStoredScriptResponse(acknowledged); } @Override public ClusterState execute(ClusterState currentState) throws Exception { ScriptMetaData smd = currentState.metaData().custom(ScriptMetaData.TYPE); smd = ScriptMetaData.putStoredScript(smd, request.id(), source); MetaData.Builder mdb = MetaData.builder(currentState.getMetaData()).putCustom(ScriptMetaData.TYPE, smd); return ClusterState.builder(currentState).metaData(mdb).build(); } }); } public void deleteStoredScript(ClusterService clusterService, DeleteStoredScriptRequest request, ActionListener<DeleteStoredScriptResponse> listener) { if (request.lang() != null && isLangSupported(request.lang()) == false) { throw new IllegalArgumentException("unable to delete stored script with unsupported lang [" + request.lang() +"]"); } clusterService.submitStateUpdateTask("delete-script-" + request.id(), new AckedClusterStateUpdateTask<DeleteStoredScriptResponse>(request, listener) { @Override protected DeleteStoredScriptResponse newResponse(boolean acknowledged) { return new DeleteStoredScriptResponse(acknowledged); } @Override public ClusterState execute(ClusterState currentState) throws Exception { ScriptMetaData smd = currentState.metaData().custom(ScriptMetaData.TYPE); smd = ScriptMetaData.deleteStoredScript(smd, request.id(), request.lang()); MetaData.Builder mdb = MetaData.builder(currentState.getMetaData()).putCustom(ScriptMetaData.TYPE, smd); return ClusterState.builder(currentState).metaData(mdb).build(); } }); } public StoredScriptSource getStoredScript(ClusterState state, GetStoredScriptRequest request) { ScriptMetaData scriptMetadata = state.metaData().custom(ScriptMetaData.TYPE); if (scriptMetadata != null) { return scriptMetadata.getStoredScript(request.id(), request.lang()); } else { return null; } } /** * Executes a previously compiled script provided as an argument */ public ExecutableScript executable(CompiledScript compiledScript, Map<String, Object> params) { return getScriptEngineServiceForLang(compiledScript.lang()).executable(compiledScript, params); } /** * Compiles (or retrieves from cache) and executes the provided search script */ public SearchScript search(SearchLookup lookup, Script script, ScriptContext scriptContext) { CompiledScript compiledScript = compile(script, scriptContext); return search(lookup, compiledScript, script.getParams()); } /** * Binds provided parameters to a compiled script returning a * {@link SearchScript} ready for execution */ public SearchScript search(SearchLookup lookup, CompiledScript compiledScript, Map<String, Object> params) { return getScriptEngineServiceForLang(compiledScript.lang()).search(compiledScript, lookup, params); } private boolean isAnyScriptContextEnabled(String lang, ScriptType scriptType) { for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) { if (canExecuteScript(lang, scriptType, scriptContext)) { return true; } } return false; } private boolean canExecuteScript(String lang, ScriptType scriptType, ScriptContext scriptContext) { assert lang != null; if (scriptContextRegistry.isSupportedContext(scriptContext) == false) { throw new IllegalArgumentException("script context [" + scriptContext.getKey() + "] not supported"); } return scriptModes.getScriptEnabled(lang, scriptType, scriptContext); } public ScriptStats stats() { return scriptMetrics.stats(); } @Override public void clusterChanged(ClusterChangedEvent event) { clusterState = event.state(); } /** * A small listener for the script cache that calls each * {@code ScriptEngine}'s {@code scriptRemoved} method when the * script has been removed from the cache */ private class ScriptCacheRemovalListener implements RemovalListener<CacheKey, CompiledScript> { @Override public void onRemoval(RemovalNotification<CacheKey, CompiledScript> notification) { if (logger.isDebugEnabled()) { logger.debug("removed {} from cache, reason: {}", notification.getValue(), notification.getRemovalReason()); } scriptMetrics.onCacheEviction(); } } private class ScriptChangesListener implements FileChangesListener { private boolean deprecationEmitted = false; private Tuple<String, String> getScriptNameExt(Path file) { Path scriptPath = scriptsDirectory.relativize(file); int extIndex = scriptPath.toString().lastIndexOf('.'); if (extIndex <= 0) { return null; } String ext = scriptPath.toString().substring(extIndex + 1); if (ext.isEmpty()) { return null; } String scriptName = scriptPath.toString().substring(0, extIndex).replace(scriptPath.getFileSystem().getSeparator(), "_"); return new Tuple<>(scriptName, ext); } @Override public void onFileInit(Path file) { Tuple<String, String> scriptNameExt = getScriptNameExt(file); if (scriptNameExt == null) { logger.debug("Skipped script with invalid extension : [{}]", file); return; } if (logger.isTraceEnabled()) { logger.trace("Loading script file : [{}]", file); } ScriptEngine engineService = getScriptEngineServiceForFileExt(scriptNameExt.v2()); if (engineService == null) { logger.warn("No script engine found for [{}]", scriptNameExt.v2()); } else { if (deprecationEmitted == false) { deprecationLogger.deprecated("File scripts are deprecated. Use stored or inline scripts instead."); deprecationEmitted = true; } try { //we don't know yet what the script will be used for, but if all of the operations for this lang // with file scripts are disabled, it makes no sense to even compile it and cache it. if (isAnyScriptContextEnabled(engineService.getType(), ScriptType.FILE)) { logger.info("compiling script file [{}]", file.toAbsolutePath()); try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(file), StandardCharsets.UTF_8)) { String script = Streams.copyToString(reader); String id = scriptNameExt.v1(); CacheKey cacheKey = new CacheKey(engineService.getType(), id, null); // pass the actual file name to the compiler (for script engines that care about this) Object executable = engineService.compile(file.getFileName().toString(), script, Collections.emptyMap()); CompiledScript compiledScript = new CompiledScript(ScriptType.FILE, id, engineService.getType(), executable); staticCache.put(cacheKey, compiledScript); scriptMetrics.onCompilation(); } } else { logger.warn("skipping compile of script file [{}] as all scripted operations are disabled for file scripts", file.toAbsolutePath()); } } catch (ScriptException e) { try (XContentBuilder builder = JsonXContent.contentBuilder()) { builder.prettyPrint(); builder.startObject(); ElasticsearchException.generateThrowableXContent(builder, ToXContent.EMPTY_PARAMS, e); builder.endObject(); logger.warn("failed to load/compile script [{}]: {}", scriptNameExt.v1(), builder.string()); } catch (IOException ioe) { ioe.addSuppressed(e); logger.warn((Supplier<?>) () -> new ParameterizedMessage( "failed to log an appropriate warning after failing to load/compile script [{}]", scriptNameExt.v1()), ioe); } /* Log at the whole exception at the debug level as well just in case the stack trace is important. That way you can * turn on the stack trace if you need it. */ logger.debug((Supplier<?>) () -> new ParameterizedMessage("failed to load/compile script [{}]. full exception:", scriptNameExt.v1()), e); } catch (Exception e) { logger.warn((Supplier<?>) () -> new ParameterizedMessage("failed to load/compile script [{}]", scriptNameExt.v1()), e); } } } @Override public void onFileCreated(Path file) { onFileInit(file); } @Override public void onFileDeleted(Path file) { Tuple<String, String> scriptNameExt = getScriptNameExt(file); if (scriptNameExt != null) { ScriptEngine engineService = getScriptEngineServiceForFileExt(scriptNameExt.v2()); assert engineService != null; logger.info("removing script file [{}]", file.toAbsolutePath()); staticCache.remove(new CacheKey(engineService.getType(), scriptNameExt.v1(), null)); } } @Override public void onFileChanged(Path file) { onFileInit(file); } } private static final class CacheKey { final String lang; final String idOrCode; final Map<String, String> options; private CacheKey(String lang, String idOrCode, Map<String, String> options) { this.lang = lang; this.idOrCode = idOrCode; this.options = options; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CacheKey cacheKey = (CacheKey)o; if (lang != null ? !lang.equals(cacheKey.lang) : cacheKey.lang != null) return false; if (!idOrCode.equals(cacheKey.idOrCode)) return false; return options != null ? options.equals(cacheKey.options) : cacheKey.options == null; } @Override public int hashCode() { int result = lang != null ? lang.hashCode() : 0; result = 31 * result + idOrCode.hashCode(); result = 31 * result + (options != null ? options.hashCode() : 0); return result; } } }