001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014 ForgeRock AS.
015 */
016package org.forgerock.openig.script;
017
018import static org.forgerock.util.Utils.*;
019
020import java.io.File;
021import java.io.FileWriter;
022import java.io.IOException;
023import java.net.URL;
024import java.util.Map;
025
026import javax.script.ScriptException;
027
028import org.forgerock.openig.config.Environment;
029
030import groovy.lang.Binding;
031import groovy.util.GroovyScriptEngine;
032
033/**
034 * A compiled script.
035 */
036public final class Script {
037    /**
038     * Groovy script implementation.
039     */
040    private final static class GroovyImpl implements Impl {
041        private final GroovyScriptEngine engine;
042        private final String fileName;
043
044        private GroovyImpl(final GroovyScriptEngine engine, final String fileName) {
045            this.engine = engine;
046            this.fileName = fileName;
047        }
048
049        @Override
050        public Object run(final Map<String, Object> bindings) throws ScriptException {
051            try {
052                return engine.run(fileName, new Binding(bindings));
053            } catch (final Exception e) {
054                throw new ScriptException(e);
055            }
056        }
057    }
058
059    private interface Impl {
060        Object run(Map<String, Object> bindings) throws ScriptException;
061    }
062
063    /**
064     * The mime-type for Groovy scripts.
065     */
066    public static final String GROOVY_MIME_TYPE = "application/x-groovy";
067
068    /**
069     * The mime-type for Javascript scripts.
070     */
071    public static final String JS_MIME_TYPE = "text/javascript";
072
073    private static final String EOL = System.getProperty("line.separator");
074
075    private static final Object INIT_LOCK = new Object();
076    /**
077     * The groovy script cache directory.
078     *
079     * @GuardedBy initializationLock
080     */
081    private static volatile File groovyScriptCacheDir = null;
082    /**
083     * The groovy script engine.
084     *
085     * @GuardedBy initializationLock
086     */
087    private static volatile GroovyScriptEngine groovyScriptEngine = null;
088
089    /**
090     * Loads a script having the provided content type and file name.
091     *
092     * @param environment The application environment.
093     * @param mimeType The script language mime-type.
094     * @param file The location of the script to be loaded.
095     * @return The script.
096     * @throws ScriptException If the script could not be loaded.
097     */
098    public static Script fromFile(final Environment environment,
099                                  final String mimeType,
100                                  final String file) throws ScriptException {
101        if (GROOVY_MIME_TYPE.equals(mimeType)) {
102            final GroovyScriptEngine engine = getGroovyScriptEngine(environment);
103            final Impl impl = new GroovyImpl(engine, file);
104            return new Script(impl);
105        } else {
106            throw new ScriptException("Invalid script mime-type '" + mimeType + "': only '"
107                    + GROOVY_MIME_TYPE + "' is supported");
108        }
109    }
110
111    /**
112     * Loads a script having the provided content type and content.
113     *
114     * @param environment The application environment.
115     * @param mimeType The script language mime-type.
116     * @param sourceLines The script content.
117     * @return The script.
118     * @throws ScriptException If the script could not be loaded.
119     */
120    public static Script fromSource(final Environment environment,
121                                    final String mimeType,
122                                    final String... sourceLines) throws ScriptException {
123        return fromSource(environment, mimeType, joinAsString(EOL, (Object[]) sourceLines));
124    }
125
126    /**
127     * Loads a script having the provided content type and content.
128     *
129     * @param environment The application environment.
130     * @param mimeType The script language mime-type.
131     * @param source The script content.
132     * @return The script.
133     * @throws ScriptException If the script could not be loaded.
134     */
135    public static Script fromSource(final Environment environment,
136                                    final String mimeType,
137                                    final String source) throws ScriptException {
138        if (GROOVY_MIME_TYPE.equals(mimeType)) {
139            final GroovyScriptEngine engine = getGroovyScriptEngine(environment);
140            final File groovyScriptCacheDir = getGroovyScriptCacheDir();
141            try {
142                final File cachedScript =
143                        File.createTempFile("script-", ".groovy", groovyScriptCacheDir);
144                cachedScript.deleteOnExit();
145                final FileWriter writer = new FileWriter(cachedScript);
146                writer.write(source);
147                writer.close();
148                final Impl impl = new GroovyImpl(engine, cachedScript.getAbsolutePath());
149                return new Script(impl);
150            } catch (final IOException e) {
151                throw new ScriptException(e);
152            }
153        } else {
154            throw new ScriptException("Invalid script mime-type '" + mimeType + "': only '"
155                    + GROOVY_MIME_TYPE + "' is supported");
156        }
157    }
158
159    private static File getGroovyScriptCacheDir() throws ScriptException {
160        File cacheDir = groovyScriptCacheDir;
161        if (cacheDir != null) {
162            return cacheDir;
163        }
164
165        synchronized (INIT_LOCK) {
166            cacheDir = groovyScriptCacheDir;
167            if (cacheDir != null) {
168                return cacheDir;
169            }
170
171            try {
172                cacheDir = File.createTempFile("openig-groovy-script-cache-", null);
173                cacheDir.delete();
174                cacheDir.mkdir();
175                cacheDir.deleteOnExit();
176            } catch (final IOException e) {
177                throw new ScriptException(e);
178            }
179
180            // Assign only after having fully initialized the cache directory.
181            groovyScriptCacheDir = cacheDir;
182            return cacheDir;
183        }
184    }
185
186    private static GroovyScriptEngine getGroovyScriptEngine(final Environment environment)
187            throws ScriptException {
188        GroovyScriptEngine engine = groovyScriptEngine;
189        if (engine != null) {
190            return engine;
191        }
192
193        synchronized (INIT_LOCK) {
194            engine = groovyScriptEngine;
195            if (engine != null) {
196                return engine;
197            }
198
199            final String classPath = environment.getScriptDirectory("groovy").getAbsolutePath();
200            try {
201                engine = new GroovyScriptEngine(classPath);
202            } catch (final IOException e) {
203                throw new ScriptException(e);
204            }
205
206            // Bootstrap the Groovy environment, e.g. add meta-classes.
207            final URL bootstrap =
208                    Script.class.getClassLoader().getResource("scripts/groovy/bootstrap.groovy");
209            try {
210                engine.run(bootstrap.toString(), new Binding());
211            } catch (Exception e) {
212                throw new ScriptException(e);
213            }
214
215            // Assign only after having fully initialized the engine.
216            groovyScriptEngine = engine;
217            return engine;
218        }
219    }
220
221    private final Impl impl;
222
223    private Script(final Impl impl) {
224        this.impl = impl;
225    }
226
227    /**
228     * Runs this script using the provided named variable bindings.
229     *
230     * @param bindings
231     *            The set of bindings to inject into the script.
232     * @return The result returned by the script.
233     * @throws ScriptException
234     *             If the script failed to execute.
235     */
236    public Object run(final Map<String, Object> bindings) throws ScriptException {
237        return impl.run(bindings);
238    }
239}