001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.script;
018
019import java.io.File;
020import java.io.Serializable;
021import java.nio.file.Path;
022import java.security.AccessController;
023import java.security.PrivilegedAction;
024import java.util.List;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.ConcurrentMap;
027
028import javax.script.Bindings;
029import javax.script.Compilable;
030import javax.script.CompiledScript;
031import javax.script.ScriptEngine;
032import javax.script.ScriptEngineFactory;
033import javax.script.ScriptEngineManager;
034import javax.script.ScriptException;
035import javax.script.SimpleBindings;
036
037import org.apache.logging.log4j.Logger;
038import org.apache.logging.log4j.core.config.Configuration;
039import org.apache.logging.log4j.core.util.FileWatcher;
040import org.apache.logging.log4j.core.util.WatchManager;
041import org.apache.logging.log4j.status.StatusLogger;
042import org.apache.logging.log4j.util.Strings;
043
044/**
045 * Manages the scripts use by the Configuration.
046 */
047public class ScriptManager implements FileWatcher, Serializable {
048
049    private abstract class AbstractScriptRunner implements ScriptRunner {
050
051        private static final String KEY_STATUS_LOGGER = "statusLogger";
052        private static final String KEY_CONFIGURATION = "configuration";
053
054        @Override
055        public Bindings createBindings() {
056            final SimpleBindings bindings = new SimpleBindings();
057            bindings.put(KEY_CONFIGURATION, configuration);
058            bindings.put(KEY_STATUS_LOGGER, logger);
059            return bindings;
060        }
061
062    }
063
064    private static final long serialVersionUID = -2534169384971965196L;
065    private static final String KEY_THREADING = "THREADING";
066    private static final Logger logger = StatusLogger.getLogger();
067
068    private final Configuration configuration;
069    private final ScriptEngineManager manager = new ScriptEngineManager();
070    private final ConcurrentMap<String, ScriptRunner> scriptRunners = new ConcurrentHashMap<>();
071    private final String languages;
072    private final WatchManager watchManager;
073
074    public ScriptManager(final Configuration configuration, final WatchManager watchManager) {
075        this.configuration = configuration;
076        this.watchManager = watchManager;
077        final List<ScriptEngineFactory> factories = manager.getEngineFactories();
078        if (logger.isDebugEnabled()) {
079            final StringBuilder sb = new StringBuilder();
080            final int factorySize = factories.size();
081            logger.debug("Installed {} script engine{}", factorySize, factorySize != 1 ? "s" : Strings.EMPTY);
082            for (final ScriptEngineFactory factory : factories) {
083                String threading = (String) factory.getParameter(KEY_THREADING);
084                if (threading == null) {
085                    threading = "Not Thread Safe";
086                }
087                final StringBuilder names = new StringBuilder();
088                final List<String> languageNames = factory.getNames();
089                for (final String name : languageNames) {
090                    if (names.length() > 0) {
091                        names.append(", ");
092                    }
093                    names.append(name);
094                }
095                if (sb.length() > 0) {
096                    sb.append(", ");
097                }
098                sb.append(names);
099                final boolean compiled = factory.getScriptEngine() instanceof Compilable;
100                logger.debug("{} version: {}, language: {}, threading: {}, compile: {}, names: {}, factory class: {}",
101                        factory.getEngineName(), factory.getEngineVersion(), factory.getLanguageName(), threading,
102                        compiled, languageNames, factory.getClass().getName());
103            }
104            languages = sb.toString();
105        } else {
106            final StringBuilder names = new StringBuilder();
107            for (final ScriptEngineFactory factory : factories) {
108                for (final String name : factory.getNames()) {
109                    if (names.length() > 0) {
110                        names.append(", ");
111                    }
112                    names.append(name);
113                }
114            }
115            languages = names.toString();
116        }
117    }
118
119    public void addScript(final AbstractScript script) {
120        final ScriptEngine engine = manager.getEngineByName(script.getLanguage());
121        if (engine == null) {
122            logger.error("No ScriptEngine found for language " + script.getLanguage() + ". Available languages are: "
123                    + languages);
124            return;
125        }
126        if (engine.getFactory().getParameter(KEY_THREADING) == null) {
127            scriptRunners.put(script.getName(), new ThreadLocalScriptRunner(script));
128        } else {
129            scriptRunners.put(script.getName(), new MainScriptRunner(engine, script));
130        }
131
132        if (script instanceof ScriptFile) {
133            final ScriptFile scriptFile = (ScriptFile) script;
134            final Path path = scriptFile.getPath();
135            if (scriptFile.isWatched() && path != null) {
136                watchManager.watchFile(path.toFile(), this);
137            }
138        }
139    }
140
141    public Bindings createBindings(final AbstractScript script) {
142        return getScriptRunner(script).createBindings();
143    }
144
145    public AbstractScript getScript(final String name) {
146        final ScriptRunner runner = scriptRunners.get(name);
147        return runner != null ? runner.getScript() : null;
148    }
149
150    @Override
151    public void fileModified(final File file) {
152        final ScriptRunner runner = scriptRunners.get(file.toString());
153        if (runner == null) {
154            logger.info("{} is not a running script");
155            return;
156        }
157        final ScriptEngine engine = runner.getScriptEngine();
158        final AbstractScript script = runner.getScript();
159        if (engine.getFactory().getParameter(KEY_THREADING) == null) {
160            scriptRunners.put(script.getName(), new ThreadLocalScriptRunner(script));
161        } else {
162            scriptRunners.put(script.getName(), new MainScriptRunner(engine, script));
163        }
164
165    }
166
167    public Object execute(final String name, final Bindings bindings) {
168        final ScriptRunner scriptRunner = scriptRunners.get(name);
169        if (scriptRunner == null) {
170            logger.warn("No script named {} could be found");
171            return null;
172        }
173        return AccessController.doPrivileged(new PrivilegedAction<Object>() {
174            @Override
175            public Object run() {
176                return scriptRunner.execute(bindings);
177            }
178        });
179    }
180
181    private interface ScriptRunner {
182
183        Bindings createBindings();
184
185        Object execute(Bindings bindings);
186
187        AbstractScript getScript();
188
189        ScriptEngine getScriptEngine();
190    }
191
192    private class MainScriptRunner extends AbstractScriptRunner {
193        private final AbstractScript script;
194        private final CompiledScript compiledScript;
195        private final ScriptEngine scriptEngine;
196
197        public MainScriptRunner(final ScriptEngine scriptEngine, final AbstractScript script) {
198            this.script = script;
199            this.scriptEngine = scriptEngine;
200            CompiledScript compiled = null;
201            if (scriptEngine instanceof Compilable) {
202                logger.debug("Script {} is compilable", script.getName());
203                compiled = AccessController.doPrivileged(new PrivilegedAction<CompiledScript>() {
204                    @Override
205                    public CompiledScript run() {
206                        try {
207                            return ((Compilable) scriptEngine).compile(script.getScriptText());
208                        } catch (final Throwable ex) {
209                            /*
210                             * ScriptException is what really should be caught here. However, beanshell's ScriptEngine
211                             * implements Compilable but then throws Error when the compile method is called!
212                             */
213                            logger.warn("Error compiling script", ex);
214                            return null;
215                        }
216                    }
217                });
218            }
219            compiledScript = compiled;
220        }
221
222        @Override
223        public ScriptEngine getScriptEngine() {
224            return this.scriptEngine;
225        }
226
227        @Override
228        public Object execute(final Bindings bindings) {
229            if (compiledScript != null) {
230                try {
231                    return compiledScript.eval(bindings);
232                } catch (final ScriptException ex) {
233                    logger.error("Error running script " + script.getName(), ex);
234                    return null;
235                }
236            }
237            try {
238                return scriptEngine.eval(script.getScriptText(), bindings);
239            } catch (final ScriptException ex) {
240                logger.error("Error running script " + script.getName(), ex);
241                return null;
242            }
243        }
244
245        @Override
246        public AbstractScript getScript() {
247            return script;
248        }
249    }
250
251    private class ThreadLocalScriptRunner extends AbstractScriptRunner {
252        private final AbstractScript script;
253
254        private final ThreadLocal<MainScriptRunner> runners = new ThreadLocal<MainScriptRunner>() {
255            @Override
256            protected MainScriptRunner initialValue() {
257                final ScriptEngine engine = manager.getEngineByName(script.getLanguage());
258                return new MainScriptRunner(engine, script);
259            }
260        };
261
262        public ThreadLocalScriptRunner(final AbstractScript script) {
263            this.script = script;
264        }
265
266        @Override
267        public Object execute(final Bindings bindings) {
268            return runners.get().execute(bindings);
269        }
270
271        @Override
272        public AbstractScript getScript() {
273            return script;
274        }
275
276        @Override
277        public ScriptEngine getScriptEngine() {
278            return runners.get().getScriptEngine();
279        }
280    }
281
282    private ScriptRunner getScriptRunner(final AbstractScript script) {
283        return scriptRunners.get(script.getName());
284    }
285}