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}