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-2015 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 static final class GroovyImpl implements Impl {
041        private final GroovyScriptEngine engine;
042        private final String fileName;
043
044        private GroovyImpl(final GroovyScriptEngine engine, final String fileName) throws ScriptException {
045            this.engine = engine;
046            this.fileName = fileName;
047            // Compile a class for the script, that will trigger a first set of errors for invalid scripts
048            try {
049                engine.loadScriptByName(fileName);
050            } catch (Exception e) {
051                throw new ScriptException(e);
052            }
053        }
054
055        @Override
056        public Object run(final Map<String, Object> bindings) throws ScriptException {
057            try {
058                return engine.run(fileName, new Binding(bindings));
059            } catch (final Exception e) {
060                throw new ScriptException(e);
061            } catch (final Throwable e) {
062                throw new ScriptException(new Exception(e));
063            }
064        }
065    }
066
067    private interface Impl {
068        Object run(Map<String, Object> bindings) throws ScriptException;
069    }
070
071    /**
072     * The mime-type for Groovy scripts.
073     */
074    public static final String GROOVY_MIME_TYPE = "application/x-groovy";
075
076    /**
077     * The mime-type for Javascript scripts.
078     */
079    public static final String JS_MIME_TYPE = "text/javascript";
080
081    private static final String EOL = System.getProperty("line.separator");
082
083    private static final Object INIT_LOCK = new Object();
084    /**
085     * The groovy script cache directory.
086     *
087     * @GuardedBy initializationLock
088     */
089    private static volatile File groovyScriptCacheDir;
090    /**
091     * The groovy script engine.
092     *
093     * @GuardedBy initializationLock
094     */
095    private static volatile GroovyScriptEngine groovyScriptEngine;
096
097    /**
098     * Loads a script having the provided content type and file name.
099     *
100     * @param environment The application environment.
101     * @param mimeType The script language mime-type.
102     * @param file The location of the script to be loaded.
103     * @return The script.
104     * @throws ScriptException If the script could not be loaded.
105     */
106    public static Script fromFile(final Environment environment,
107                                  final String mimeType,
108                                  final String file) throws ScriptException {
109        if (GROOVY_MIME_TYPE.equals(mimeType)) {
110            final GroovyScriptEngine engine = getGroovyScriptEngine(environment);
111            final Impl impl = new GroovyImpl(engine, file);
112            return new Script(impl);
113        } else {
114            throw new ScriptException("Invalid script mime-type '" + mimeType + "': only '"
115                    + GROOVY_MIME_TYPE + "' is supported");
116        }
117    }
118
119    /**
120     * Loads a script having the provided content type and content.
121     *
122     * @param environment The application environment.
123     * @param mimeType The script language mime-type.
124     * @param sourceLines The script content.
125     * @return The script.
126     * @throws ScriptException If the script could not be loaded.
127     */
128    public static Script fromSource(final Environment environment,
129                                    final String mimeType,
130                                    final String... sourceLines) throws ScriptException {
131        return fromSource(environment, mimeType, joinAsString(EOL, (Object[]) sourceLines));
132    }
133
134    /**
135     * Loads a script having the provided content type and content.
136     *
137     * @param environment The application environment.
138     * @param mimeType The script language mime-type.
139     * @param source The script content.
140     * @return The script.
141     * @throws ScriptException If the script could not be loaded.
142     */
143    public static Script fromSource(final Environment environment,
144                                    final String mimeType,
145                                    final String source) throws ScriptException {
146        if (GROOVY_MIME_TYPE.equals(mimeType)) {
147            final GroovyScriptEngine engine = getGroovyScriptEngine(environment);
148            final File groovyScriptCacheDir = getGroovyScriptCacheDir();
149            try {
150                final File cachedScript =
151                        File.createTempFile("script-", ".groovy", groovyScriptCacheDir);
152                cachedScript.deleteOnExit();
153                final FileWriter writer = new FileWriter(cachedScript);
154                writer.write(source);
155                writer.close();
156                final Impl impl = new GroovyImpl(engine, cachedScript.toURI().toURL().toString());
157                return new Script(impl);
158            } catch (final IOException e) {
159                throw new ScriptException(e);
160            }
161        } else {
162            throw new ScriptException("Invalid script mime-type '" + mimeType + "': only '"
163                    + GROOVY_MIME_TYPE + "' is supported");
164        }
165    }
166
167    private static File getGroovyScriptCacheDir() throws ScriptException {
168        File cacheDir = groovyScriptCacheDir;
169        if (cacheDir != null) {
170            return cacheDir;
171        }
172
173        synchronized (INIT_LOCK) {
174            cacheDir = groovyScriptCacheDir;
175            if (cacheDir != null) {
176                return cacheDir;
177            }
178
179            try {
180                cacheDir = File.createTempFile("openig-groovy-script-cache-", null);
181                cacheDir.delete();
182                cacheDir.mkdir();
183                cacheDir.deleteOnExit();
184            } catch (final IOException e) {
185                throw new ScriptException(e);
186            }
187
188            // Assign only after having fully initialized the cache directory.
189            groovyScriptCacheDir = cacheDir;
190            return cacheDir;
191        }
192    }
193
194    private static GroovyScriptEngine getGroovyScriptEngine(final Environment environment)
195            throws ScriptException {
196        GroovyScriptEngine engine = groovyScriptEngine;
197        if (engine != null) {
198            return engine;
199        }
200
201        synchronized (INIT_LOCK) {
202            engine = groovyScriptEngine;
203            if (engine != null) {
204                return engine;
205            }
206
207            final String classPath = environment.getScriptDirectory("groovy").getAbsolutePath();
208            try {
209                engine = new GroovyScriptEngine(classPath);
210            } catch (final IOException e) {
211                throw new ScriptException(e);
212            }
213
214            // Bootstrap the Groovy environment, e.g. add meta-classes.
215            final URL bootstrap =
216                    Script.class.getClassLoader().getResource("scripts/groovy/bootstrap.groovy");
217            try {
218                engine.run(bootstrap.toString(), new Binding());
219            } catch (Exception e) {
220                throw new ScriptException(e);
221            }
222
223            // Assign only after having fully initialized the engine.
224            groovyScriptEngine = engine;
225            return engine;
226        }
227    }
228
229    private final Impl impl;
230
231    private Script(final Impl impl) {
232        this.impl = impl;
233    }
234
235    /**
236     * Runs this script using the provided named variable bindings.
237     *
238     * @param bindings
239     *            The set of bindings to inject into the script.
240     * @return The result returned by the script.
241     * @throws ScriptException
242     *             If the script failed to execute.
243     */
244    public Object run(final Map<String, Object> bindings) throws ScriptException {
245        return impl.run(bindings);
246    }
247}