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}