/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2013-2015 ForgeRock AS.
*/
package org.forgerock.openidm.script;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newQueryResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import javax.script.Bindings;
import javax.script.ScriptException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CountPolicy;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourcePath;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.ServiceUnavailableException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.smartevent.EventEntry;
import org.forgerock.openidm.smartevent.Name;
import org.forgerock.openidm.smartevent.Publisher;
import org.forgerock.script.Scope;
import org.forgerock.script.Script;
import org.forgerock.script.ScriptEntry;
import org.forgerock.script.exception.ScriptThrownException;
import org.forgerock.script.scope.Function;
import org.forgerock.script.scope.FunctionFactory;
import org.forgerock.script.scope.Parameter;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A ScriptedRequestHandler implements a RequestHandler using a script.
*/
public class ScriptedRequestHandler implements Scope, RequestHandler {
/**
* Setup logging for the {@link ScriptedRequestHandler}.
*/
private static final Logger logger = LoggerFactory.getLogger(ScriptedRequestHandler.class);
private final AtomicReference<ScriptEntry> scriptEntry;
private final ScriptCustomizer customizer;
public ScriptedRequestHandler(final ScriptEntry scriptEntry, final ScriptCustomizer customizer) {
if (null == scriptEntry) {
throw new NullPointerException();
}
if (null == customizer) {
throw new NullPointerException();
}
this.scriptEntry = new AtomicReference<ScriptEntry>(scriptEntry);
this.customizer = customizer;
}
private ScriptEntry getScriptEntry() {
return scriptEntry.get();
}
/**
* Eventually set the script value.
* <p/>
*
* @param newScriptEntry
* @throws NullPointerException
* when the {@code newScriptEntry} is null.
*/
public void setScriptEntry(final ScriptEntry newScriptEntry) {
if (null == newScriptEntry) {
throw new NullPointerException();
}
scriptEntry.lazySet(newScriptEntry);
}
// ----- Implementation of Scope interface
@Override
public void put(final String key, final Object value) {
getScriptEntry().put(key, value);
}
@Override
public Object get(String key) {
return getScriptEntry().get(key);
}
@Override
public Bindings getBindings() {
return getScriptEntry().getBindings();
}
@Override
public void setBindings(final Bindings bindings) {
getScriptEntry().setBindings(bindings);
}
@Override
public void flush() {
getScriptEntry().flush();
}
@Override
public Bindings createBindings() {
return getScriptEntry().createBindings();
}
// ----- Implementation of RequestHandler interface
public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/action"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleAction(context, request, script.getBindings());
Object result = script.eval();
if (null == result) {
return newActionResponse(new JsonValue(null)).asPromise();
} else if (result instanceof JsonValue) {
return newActionResponse((JsonValue) result).asPromise();
} else if (result instanceof Map) {
return newActionResponse(new JsonValue(result)).asPromise();
} else {
JsonValue resource = new JsonValue(new HashMap<String, Object>(1));
resource.put("result", result);
return newActionResponse(new JsonValue(result)).asPromise();
}
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/create"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleCreate(context, request, script.getBindings());
return evaluate(request, script);
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/delete"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleDelete(context, request, script.getBindings());
return evaluate(request, script);
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/patch"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handlePatch(context, request, script.getBindings());
return evaluate(request, script);
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
/**
* TODO Implement this method
*
* {@inheritDoc}
*/
public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
final QueryResourceHandler handler) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/query"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleQuery(context, request, script.getBindings());
final Function<Void> queryCallback = new Function<Void>() {
@Override
public Void call(Parameter scope, Function<?> callback, Object... arguments)
throws ResourceException, NoSuchMethodException {
if (arguments.length == 3 && null != arguments[2]) {
if (arguments[2] instanceof Map) {
}
if (arguments[2] instanceof JsonValue) {
} else {
throw new NoSuchMethodException(FunctionFactory
.getNoSuchMethodMessage("callback", arguments));
}
} else if (arguments.length >= 2 && null != arguments[1]) {
if (arguments[1] instanceof Map) {
}
if (arguments[1] instanceof JsonValue) {
} else {
throw new NoSuchMethodException(FunctionFactory
.getNoSuchMethodMessage("callback", arguments));
}
} else if (arguments.length >= 1 && null != arguments[0]) {
if (arguments[0] instanceof Map) {
}
if (arguments[0] instanceof JsonValue) {
} else {
throw new NoSuchMethodException(FunctionFactory
.getNoSuchMethodMessage("callback", arguments));
}
} else {
throw new NoSuchMethodException(FunctionFactory.getNoSuchMethodMessage(
"callback", arguments));
}
return null;
}
};
script.putSafe("callback", queryCallback);
Object rawResult = script.eval();
JsonValue result = null;
if (rawResult instanceof JsonValue) {
result = (JsonValue) rawResult;
} else {
result = new JsonValue(rawResult);
}
QueryResponse queryResponse = newQueryResponse();
// Script can either
// - return null and instead use callback hook to call
// handleResource, handleResult, handleError
// careful! script MUST call handleResult or handleError itself
// or
// - return a result list of resources
// or
// - return a full query result structure
if (!result.isNull()) {
if (result.isList()) {
// Script may return just the result elements as a list
handleQueryResultList(result, handler);
} else {
// Or script may return a full query response structure,
// with meta-data and results field
if (result.isDefined(QueryResponse.FIELD_RESULT)) {
handleQueryResultList(result.get(QueryResponse.FIELD_RESULT), handler);
queryResponse = newQueryResponse(
result.get(QueryResponse.FIELD_PAGED_RESULTS_COOKIE).asString(),
result.get(QueryResponse.FIELD_TOTAL_PAGED_RESULTS_POLICY).asEnum(CountPolicy.class),
result.get(QueryResponse.FIELD_TOTAL_PAGED_RESULTS).asInteger());
} else {
logger.debug("Script returned unexpected query result structure: ",
result.getObject());
return new InternalServerErrorException(
"Script returned unexpected query result structure of type "
+ result.getObject().getClass())
.asPromise();
}
}
}
return queryResponse.asPromise();
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
/**
* Takes a list of results (in json value wrapped form) and
* calls the handleResource for each on the handler.
* @param resultList the list of results, possibly with id and rev entries
* @param handler the handle to set the results on
*/
private void handleQueryResultList(JsonValue resultList, QueryResourceHandler handler) {
for (JsonValue entry : resultList) {
// These can end up null
String id = null;
String rev = null;
if (entry.isMap()) {
id = entry.get(ResourceResponse.FIELD_ID).asString();
rev = entry.get(ResourceResponse.FIELD_REVISION).asString();
}
handler.handleResource(newResourceResponse(id, rev, entry));
}
}
public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/read"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleRead(context, request, script.getBindings());
return evaluate(request, script);
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
EventEntry measure = Publisher.start(Name.get("openidm/internal/script/" + this.getScriptEntry().getName().getName() + "/update"), null, null);
try {
final ScriptEntry _scriptEntry = getScriptEntry();
if (!_scriptEntry.isActive()) {
throw new ServiceUnavailableException("Inactive script: " + _scriptEntry.getName());
}
final Script script = _scriptEntry.getScript(context);
script.setBindings(script.createBindings());
customizer.handleUpdate(context, request, script.getBindings());
return evaluate(request, script);
} catch (ScriptException e) {
return convertScriptException(e).asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
} finally {
measure.end();
}
}
protected ResourceException convertScriptException(final ScriptException scriptException) {
ResourceException convertedError;
try {
throw scriptException;
} catch (ScriptThrownException e) {
convertedError = e.toResourceException(ResourceException.INTERNAL_ERROR, scriptException.getMessage());
} catch (ScriptException e) {
convertedError = new InternalServerErrorException(scriptException.getMessage(), scriptException);
}
if (convertedError.getDetail().isNull()) {
convertedError.setDetail(new JsonValue(new HashMap<String, Object>()));
}
final JsonValue detail = convertedError.getDetail();
if (detail.get("fileName").isNull()
&& detail.get("lineNumber").isNull()
&& detail.get("columnNumber").isNull()) {
detail.put("fileName", scriptException.getFileName());
detail.put("lineNumber", scriptException.getLineNumber());
detail.put("columnNumber", scriptException.getColumnNumber());
}
return convertedError;
}
private Promise<ResourceResponse, ResourceException> evaluate(final Request request, final Script script)
throws ScriptException {
Object result = script.eval();
ResourcePath resourcePath = request.getResourcePathObject();
if (null == result) {
return new NotFoundException("script returned null").asPromise();
}
JsonValue resultJson = (result instanceof JsonValue)
? (JsonValue) result
: new JsonValue(result);
// If the resultJson isn't able to provide an ID, then we default to the resourcePath.
String id = resultJson.get(ResourceResponse.FIELD_CONTENT_ID).defaultTo("").asString();
if (id.isEmpty() && resourcePath.size() > 0) {
id = resourcePath.leaf();
}
return newResourceResponse(id, null, resultJson).asPromise();
}
}