AnonymousProcessService.java
/*
* 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 2015-2017 ForgeRock AS.
*/
package org.forgerock.selfservice.core;
import static org.forgerock.json.JsonValue.field;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.selfservice.core.ServiceUtils.emptyJson;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.AbstractRequestHandler;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.selfservice.core.config.ProcessInstanceConfig;
import org.forgerock.selfservice.core.config.StageConfig;
import org.forgerock.selfservice.core.config.StageConfigException;
import org.forgerock.tokenhandler.TokenHandler;
import org.forgerock.selfservice.core.snapshot.SnapshotTokenHandlerFactory;
import org.forgerock.services.context.Context;
import org.forgerock.util.Reject;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.List;
/**
* Anonymous process service progresses a chain of {@link ProgressStage}
* configurations, handling any required client interactions.
*
* @since 0.1.0
*/
public final class AnonymousProcessService extends AbstractRequestHandler {
private static final Logger logger = LoggerFactory.getLogger(AnonymousProcessService.class);
private static final String SUBMIT_ACTION = "submitRequirements";
private static final String TOKEN_FIELD = "token";
private static final String INPUT_FIELD = "input";
private static final String TYPE_FIELD = "type";
private static final String TAG_FIELD = "tag";
private static final String STATUS_FIELD = "status";
private static final String SUCCESS_FIELD = "success";
private static final String REQUIREMENTS_FIELD = "requirements";
private static final String ADDITIONS_FIELD = "additions";
private static final String END_VALUE = "end";
private final ProgressStageBinder progressStageBinder;
private final List<StageConfig> stageConfigs;
private final SnapshotAuthor snapshotAuthor;
private final int configVersion;
/**
* Initialises the anonymous process service with the passed config.
*
* @param config
* service configuration
* @param progressStageProvider
* progress stage provider
* @param tokenHandlerFactory
* snapshot token handler factory
* @param processStore
* store for locally persisted state
* @param classLoader
* class loader used for dynamic stage class instantiation
*/
@Inject
public AnonymousProcessService(ProcessInstanceConfig config, ProgressStageProvider progressStageProvider,
SnapshotTokenHandlerFactory tokenHandlerFactory, ProcessStore processStore, ClassLoader classLoader) {
Reject.ifNull(config, progressStageProvider, tokenHandlerFactory, processStore);
Reject.ifNull(config.getStageConfigs(), config.getSnapshotTokenConfig(), config.getStorageType());
Reject.ifTrue(config.getStageConfigs().isEmpty());
progressStageBinder = new ProgressStageBinder(progressStageProvider, classLoader);
stageConfigs = config.getStageConfigs();
configVersion = config.hashCode();
TokenHandler snapshotTokenHandler = tokenHandlerFactory.get(config.getSnapshotTokenConfig());
snapshotAuthor = config.getStorageType().newSnapshotAuthor(snapshotTokenHandler, processStore);
}
@Override
public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
try {
JsonValue clientResponse = initiateProcess(new SelfServiceContext(context), request);
String revision = String.valueOf(clientResponse.getObject().hashCode());
return newResourceResponse("1", revision, clientResponse).asPromise();
} catch (ResourceException | RuntimeException e) {
return logAndAdaptException(e).asPromise();
}
}
@Override
public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
if (SUBMIT_ACTION.equals(request.getAction())) {
try {
JsonValue clientResponse = progressProcess(new SelfServiceContext(context), request);
return newActionResponse(clientResponse).asPromise();
} catch (ResourceException | RuntimeException e) {
return logAndAdaptException(e).asPromise();
}
}
return new NotSupportedException("Unknown action " + request.getAction()).asPromise();
}
private ResourceException logAndAdaptException(Exception exception) {
try {
throw exception;
} catch (InternalServerErrorException iseE) {
logger.error("Internal error intercepted", iseE);
return iseE;
} catch (ResourceException rE) {
logger.debug("Resource exception intercepted", rE);
return rE;
} catch (JsonValueException jvE) {
logger.debug("Invalid JSON input", jvE);
return new BadRequestException(jvE.getMessage(), jvE);
} catch (Exception ex) {
logger.error("Exception intercepted", ex);
return new InternalServerErrorException("Exception intercepted", ex);
}
}
/*
* Responsible for retrieving the requirements from the first stage in the flow.
*/
private JsonValue initiateProcess(Context requestContext, ReadRequest request) throws ResourceException {
ProcessContextImpl context = ProcessContextImpl
.newBuilder(requestContext, request)
.setConfigVersion(configVersion)
.build();
ProgressStageBinding<?> stage = retrieveStage(context);
JsonValue requirements = stage.gatherInitialRequirements(context);
if (logger.isDebugEnabled()) {
logger.debug("Initial requirements retrieved for stage " + stage.getName());
}
return renderRequirements(
stage,
StageResponse
.newBuilder()
.setRequirements(requirements)
.build());
}
/*
* With the process flow already kicked off, progresses to the flow by processing the client input.
*/
private JsonValue progressProcess(Context requestContext, ActionRequest request) throws ResourceException {
JsonValue clientInput = request.getContent();
JsonValue snapshotTokenValue = clientInput.get(TOKEN_FIELD);
ProcessContextImpl.Builder contextBuilder;
if (snapshotTokenValue.isNotNull()) {
JsonValue jsonContext = snapshotAuthor.retrieveSnapshotFrom(snapshotTokenValue.asString());
contextBuilder = ProcessContextImpl.newBuilder(requestContext, request, jsonContext);
} else {
contextBuilder = ProcessContextImpl.newBuilder(requestContext, request)
.setConfigVersion(configVersion);
}
JsonValue input = clientInput.get(INPUT_FIELD);
if (input.isNull()) {
throw new BadRequestException("No input provided");
}
ProcessContextImpl context = contextBuilder
.setInput(input)
.build();
if (configVersion != context.getConfigVersion()) {
throw new BadRequestException("Invalid token");
}
ProgressStageBinding<?> stage = retrieveStage(context);
if (logger.isDebugEnabled()) {
logger.debug("Advancing stage " + stage.getName());
}
return enactContext(context, stage);
}
private JsonValue enactContext(ProcessContextImpl context, ProgressStageBinding<?> stage) throws ResourceException {
StageResponse response = stage.advance(context);
if (response.hasRequirements()) {
// Stage has additional requirements, render response.
return renderRequirementsWithSnapshot(context, stage, response);
}
return handleProgression(context, stage);
}
private JsonValue handleProgression(ProcessContextImpl context,
ProgressStageBinding<?> stage) throws ResourceException {
if (context.getStageIndex() + 1 == stageConfigs.size()) {
// Flow complete, render completion response.
return renderCompletion(context, stage);
}
// Stage satisfied, move onto the next stage.
int nextIndex = context.getStageIndex() + 1;
ProcessContextImpl nextContext = ProcessContextImpl
.newBuilder(context.getRequestContext(), context.getRequest())
.setStageIndex(nextIndex)
.setConfigVersion(context.getConfigVersion())
.setState(context.getState())
.build();
ProgressStageBinding<?> nextStage = retrieveStage(nextContext);
JsonValue requirements = nextStage.gatherInitialRequirements(nextContext);
if (logger.isDebugEnabled()) {
logger.debug("Initial requirements retrieved for stage " + nextStage.getName());
}
if (requirements.size() > 0) {
// Stage has some initial requirements, render response.
return renderRequirementsWithSnapshot(
nextContext,
nextStage,
StageResponse
.newBuilder()
.setRequirements(requirements)
.build());
}
return enactContext(nextContext, nextStage);
}
private JsonValue renderRequirementsWithSnapshot(ProcessContextImpl context, ProgressStageBinding<?> stage,
StageResponse response) throws ResourceException {
ProcessContextImpl updatedContext = ProcessContextImpl
.newBuilder(context)
.setStageTag(response.getStageTag())
.build();
String snapshotToken = snapshotAuthor.captureSnapshotOf(updatedContext.toJson());
if (response.hasCallback()) {
response.getCallback().snapshotTokenPreview(updatedContext, snapshotToken);
}
return renderRequirements(stage, response)
.add(TOKEN_FIELD, snapshotToken);
}
private JsonValue renderRequirements(ProgressStageBinding<?> stage, StageResponse response) {
return json(
object(
field(TYPE_FIELD, stage.getName()),
field(TAG_FIELD, response.getStageTag()),
field(REQUIREMENTS_FIELD, response.getRequirements().asMap())));
}
private JsonValue renderCompletion(ProcessContextImpl context, ProgressStageBinding<?> stage) {
JsonValue response = json(
object(
field(TYPE_FIELD, stage.getName()),
field(TAG_FIELD, END_VALUE),
field(STATUS_FIELD,
object(
field(SUCCESS_FIELD, true)))));
JsonValue successAdditions = context.hasSuccessAdditions()
? context.getSuccessAdditions()
: emptyJson();
response.add(ADDITIONS_FIELD, successAdditions.asMap());
return response;
}
private ProgressStageBinding<?> retrieveStage(ProcessContextImpl context) {
if (context.getStageIndex() >= stageConfigs.size()) {
throw new StageConfigException("Invalid stage index " + context.getStageIndex());
}
StageConfig config = stageConfigs.get(context.getStageIndex());
return progressStageBinder.getBinding(config);
}
}