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.openig.config.Environment.*; 019import static org.forgerock.openig.http.HttpClient.*; 020 021import java.io.IOException; 022import java.util.HashMap; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.concurrent.ConcurrentHashMap; 026 027import javax.script.ScriptException; 028 029import org.forgerock.json.fluent.JsonValueException; 030import org.forgerock.openig.config.Environment; 031import org.forgerock.openig.handler.Handler; 032import org.forgerock.openig.handler.HandlerException; 033import org.forgerock.openig.heap.GenericHeapObject; 034import org.forgerock.openig.heap.GenericHeaplet; 035import org.forgerock.openig.heap.HeapException; 036import org.forgerock.openig.http.Exchange; 037import org.forgerock.openig.http.HttpClient; 038import org.forgerock.openig.ldap.LdapClient; 039 040/** 041 * An abstract scriptable heap object which should be used as the base class for 042 * implementing {@link org.forgerock.openig.filter.Filter filters} and {@link Handler handlers}. This heap 043 * object acts as a simple wrapper around the scripting engine. Scripts are 044 * provided with the following variable bindings: 045 * <ul> 046 * <li>{@link Map globals} - the Map of global variables which persist across 047 * successive invocations of the script 048 * <li>{@link Exchange exchange} - the HTTP exchange 049 * <li>{@link HttpClient http} - an OpenIG HTTP client which may be used for 050 * performing outbound HTTP requests 051 * <li>{@link LdapClient ldap} - an OpenIG LDAP client which may be used for 052 * performing LDAP requests such as LDAP authentication 053 * <li>{@link org.forgerock.openig.log.Logger logger} - the OpenIG logger 054 * <li>{@link Handler next} - if the heap object is a filter then this variable 055 * will contain the next handler in the filter chain. 056 * </ul> 057 * <p> 058 * <b>NOTE:</b> at the moment only Groovy is supported. 059 */ 060public abstract class AbstractScriptableHeapObject extends GenericHeapObject { 061 062 /** Creates and initializes a capture filter in a heap environment. */ 063 protected static abstract class AbstractScriptableHeaplet extends GenericHeaplet { 064 private static final String CONFIG_OPTION_FILE = "file"; 065 private static final String CONFIG_OPTION_SOURCE = "source"; 066 private static final String CONFIG_OPTION_TYPE = "type"; 067 private static final String CONFIG_OPTION_ARGS = "args"; 068 069 @Override 070 public Object create() throws HeapException { 071 final Script script = compileScript(); 072 final AbstractScriptableHeapObject component = newInstance(script); 073 HttpClient httpClient = heap.resolve(config.get("httpClient") 074 .defaultTo(HTTP_CLIENT_HEAP_KEY), 075 HttpClient.class); 076 component.setHttpClient(httpClient); 077 if (config.isDefined(CONFIG_OPTION_ARGS)) { 078 component.setArgs(config.get(CONFIG_OPTION_ARGS).asMap()); 079 } 080 return component; 081 } 082 083 /** 084 * Creates the new heap object instance using the provided script. 085 * 086 * @param script The compiled script. 087 * @return The new heap object instance using the provided script. 088 * @throws HeapException if an exception occurred during creation of the heap 089 * object or any of its dependencies. 090 * @throws JsonValueException if the heaplet (or one of its dependencies) has a 091 * malformed configuration. 092 */ 093 protected abstract AbstractScriptableHeapObject newInstance(final Script script) 094 throws HeapException; 095 096 private final Script compileScript() throws HeapException { 097 final Environment environment = heap.get(ENVIRONMENT_HEAP_KEY, Environment.class); 098 099 if (!config.isDefined(CONFIG_OPTION_TYPE)) { 100 throw new JsonValueException(config, "The configuration option '" 101 + CONFIG_OPTION_TYPE 102 + "' is required and must specify the script mime-type"); 103 } 104 final String mimeType = config.get(CONFIG_OPTION_TYPE).asString(); 105 if (config.isDefined(CONFIG_OPTION_SOURCE)) { 106 if (config.isDefined(CONFIG_OPTION_FILE)) { 107 throw new JsonValueException(config, "Both configuration options '" 108 + CONFIG_OPTION_SOURCE + "' and '" + CONFIG_OPTION_FILE 109 + "' were specified, when at most one is allowed"); 110 } 111 final String source = config.get(CONFIG_OPTION_SOURCE).asString(); 112 try { 113 return Script.fromSource(environment, mimeType, source); 114 } catch (final ScriptException e) { 115 throw new JsonValueException(config, 116 "Unable to compile the script defined in '" + CONFIG_OPTION_SOURCE 117 + "'", e 118 ); 119 } 120 } else if (config.isDefined(CONFIG_OPTION_FILE)) { 121 final String script = config.get(CONFIG_OPTION_FILE).asString(); 122 try { 123 return Script.fromFile(environment, mimeType, script); 124 } catch (final ScriptException e) { 125 throw new JsonValueException(config, "Unable to compile the script in file '" 126 + script + "'", e); 127 } 128 } else { 129 throw new JsonValueException(config, "Neither of the configuration options '" 130 + CONFIG_OPTION_SOURCE + "' and '" + CONFIG_OPTION_FILE 131 + "' were specified"); 132 } 133 } 134 135 } 136 137 // TODO: add support for periodically refreshing the Groovy script file. 138 // TODO: json/xml/sql/crest bindings. 139 140 private final Script compiledScript; 141 private HttpClient httpClient; 142 private final LdapClient ldapClient = LdapClient.getInstance(); 143 private final Map<String, Object> scriptGlobals = new ConcurrentHashMap<String, Object>(); 144 private Map<String, Object> args; 145 146 /** 147 * Creates a new scriptable heap object using the provided compiled script. 148 * 149 * @param compiledScript The compiled script. 150 */ 151 protected AbstractScriptableHeapObject(final Script compiledScript) { 152 this.compiledScript = compiledScript; 153 } 154 155 /** 156 * Sets the HTTP client which should be made available to scripts. 157 * 158 * @param client The HTTP client which should be made available to scripts. 159 */ 160 public void setHttpClient(final HttpClient client) { 161 this.httpClient = client; 162 } 163 164 /** 165 * Sets the parameters which should be made available to scripts. 166 * 167 * @param args The parameters which should be made available to scripts. 168 */ 169 public void setArgs(final Map<String, Object> args) { 170 this.args = args; 171 } 172 173 /** 174 * Runs the compiled script using the provided exchange and optional 175 * forwarding handler. 176 * 177 * @param exchange The HTTP exchange. 178 * @param next The next handler in the chain if applicable, may be 179 * {@code null}. 180 * @throws HandlerException If an error occurred while evaluating the script. 181 * @throws IOException If an I/O exception occurs. 182 */ 183 protected final void runScript(final Exchange exchange, final Handler next) 184 throws HandlerException, IOException { 185 186 try { 187 compiledScript.run(createBindings(exchange, next)); 188 } catch (final ScriptException e) { 189 if (e.getCause() instanceof HandlerException) { 190 /* 191 * This may result from invoking the next handler (for filters), 192 * or it may have been generated intentionally by the script. 193 * Either way, just pass it back up the chain. 194 */ 195 throw (HandlerException) e.getCause(); 196 } 197 198 /* 199 * The exception was unintentional: we could throw the cause or the 200 * script exception. Let's throw the script exception because it may 201 * contain useful line number information. 202 */ 203 throw new HandlerException("Script failed unexpectedly", e); 204 } 205 } 206 207 private Map<String, Object> createBindings(final Exchange exchange, final Handler next) { 208 // Set engine bindings. 209 final Map<String, Object> bindings = new HashMap<String, Object>(); 210 bindings.put("exchange", exchange); 211 bindings.put("logger", logger); 212 bindings.put("globals", scriptGlobals); 213 if (httpClient != null) { 214 bindings.put("http", httpClient); 215 } 216 bindings.put("ldap", ldapClient); 217 if (next != null) { 218 bindings.put("next", next); 219 } 220 if (args != null) { 221 for (final Entry<String, Object> entry : args.entrySet()) { 222 bindings.put(entry.getKey(), entry.getValue()); 223 } 224 } 225 226 // Redirect streams? E.g. in = request entity, out = response entity? 227 return bindings; 228 } 229}