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