/*
* 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]".
*
* Portions copyright 2012-2015 ForgeRock AS.
*/
package org.forgerock.openidm.scheduler.impl;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.openidm.util.ResourceUtil.notSupported;
import javax.script.ScriptException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.forgerock.audit.events.AuditEvent;
import org.forgerock.openidm.router.IDMConnectionFactory;
import org.forgerock.services.context.Context;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
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.RequestHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.core.IdentityServer;
import org.forgerock.openidm.core.ServerConstants;
import org.forgerock.openidm.quartz.impl.ExecutionException;
import org.forgerock.openidm.quartz.impl.ScheduledService;
import org.forgerock.script.ScriptEntry;
import org.forgerock.script.ScriptRegistry;
import org.forgerock.util.promise.Promise;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "org.forgerock.openidm.taskscanner", policy = ConfigurationPolicy.IGNORE, immediate = true)
@Properties({
@Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM TaskScanner Service"),
@Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME),
@Property(name = ServerConstants.ROUTER_PREFIX, value = "/taskscanner*")
})
@Service
public class TaskScannerService implements RequestHandler, ScheduledService {
private final static Logger logger = LoggerFactory.getLogger(TaskScannerService.class);
private final static String INVOKE_CONTEXT = "invokeContext";
private int maxCompletedRuns;
/**
* Map from TaskScanID ID to the run itself
* In historical start order, oldest first.
*/
Map<String, TaskScannerContext> taskScanRuns =
Collections.synchronizedMap(new LinkedHashMap<String, TaskScannerContext>());
/** The Connection Factory */
@Reference(policy = ReferencePolicy.STATIC)
protected IDMConnectionFactory connectionFactory;
protected void bindConnectionFactory(IDMConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
@Reference(policy = ReferencePolicy.DYNAMIC)
private ScriptRegistry scriptRegistry;
@Activate
public void activate(ComponentContext context) {
String maxCompletedStr =
IdentityServer.getInstance().getProperty("openidm.taskscanner.maxcompletedruns", "100");
maxCompletedRuns = Integer.parseInt(maxCompletedStr);
}
/**
* Invoked by the Task Scanner whenever the task scanner is triggered by the scheduler
*/
@Override
public void execute(Context context, Map<String, Object> contextMap) throws ExecutionException {
String invokerName = (String) contextMap.get(INVOKER_NAME);
String scriptName = (String) contextMap.get(CONFIG_NAME);
JsonValue params = new JsonValue(contextMap).get(CONFIGURED_INVOKE_CONTEXT);
startTaskScanJob(context, invokerName, scriptName, params);
}
@Override
public void auditScheduledService(final Context context, final AuditEvent auditEvent)
throws ExecutionException {
try {
connectionFactory.getConnection().create(
context, Requests.newCreateRequest("audit/access", auditEvent.getValue()));
} catch (ResourceException e) {
logger.error("Unable to audit scheduled service {}", auditEvent.toString());
throw new ExecutionException("Unable to audit scheduled service", e);
}
}
@Override
public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) {
String id = request.getResourcePath();
Map<String, Object> result = new LinkedHashMap<String, Object>();
if (request.getResourcePathObject().isEmpty()) {
List<Map<String, Object>> taskList = new ArrayList<Map<String, Object>>();
for (TaskScannerContext entry : taskScanRuns.values()) {
Map<String, Object> taskData = buildTaskData(entry);
taskList.add(taskData);
}
result.put("tasks", taskList);
} else {
TaskScannerContext foundRun = taskScanRuns.get(request.getResourcePath());
if (foundRun == null) {
return new NotFoundException("Task with id '" + request.getResourcePath() + "' not found.").asPromise();
}
result = buildTaskData(foundRun);
}
return newResourceResponse(id, null, new JsonValue(result)).asPromise();
}
// TODO maybe move this into TaskScannerContext?
private Map<String, Object> buildTaskData(TaskScannerContext entry) {
Map<String, Object> result = new LinkedHashMap<String, Object>();
result.put("_id", entry.getTaskScanID());
result.put("progress", entry.getProgress());
result.put("started", entry.getStatistics().getJobStartTime());
result.put("ended", entry.getStatistics().getJobEndTime());
return result;
}
@Override
public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) {
Map<String, String> params = request.getAdditionalParameters();
Map<String, Object> result = new LinkedHashMap<String, Object>();
String action = request.getAction();
if (request.getResourcePathObject().isEmpty()) {
try {
if ("execute".equalsIgnoreCase(action)) {
try {
result.put("_id", onExecute(context, request.getResourcePath(), params));
} catch (JsonProcessingException e) {
return new InternalServerErrorException(e).asPromise();
} catch (IOException e) {
return new InternalServerErrorException(e).asPromise();
}
} else {
return new BadRequestException("Unknown action: " + action).asPromise();
}
} catch (ScriptException e) {
return new InternalServerErrorException(e).asPromise();
} catch (ExecutionException e) {
logger.warn(e.getMessage());
return new BadRequestException(e.getMessage(), e).asPromise();
}
} else {
// operation on individual resource
TaskScannerContext foundRun = taskScanRuns.get(request.getResourcePath());
if (foundRun == null) {
return new NotFoundException("Task with id '" + request.getResourcePath() + "' not found.").asPromise();
}
if ("cancel".equalsIgnoreCase(action)) {
if (foundRun.isCompleted()) {
result.put("status", "FAILURE");
} else {
foundRun.cancel();
result.put("status", "SUCCESS");
}
result.put("_id", foundRun.getTaskScanID());
result.put("action", action);
} else {
return new BadRequestException("Action '" + action + "' on Task '" + request.getResourcePath()
+ "' not supported " + params)
.asPromise();
}
}
return newActionResponse(new JsonValue(result)).asPromise();
}
/**
* Performs the "execute" action, executing a supplied configuration
*
* Expects a field "name" containing the name of some config object that can be found via
* a read on "config/" + name <br><br>
*
* <b><i>e.g.</b></i> "taskscan/sunset" => "config/taskscan/sunset" => "[openidm-directory]/conf/taskscan-sunset.json"<br>
*
* @param id the id to perform the action on
* @param params field contaning the parameters of execution
* @return the set of parameters supplied
* @throws ExecutionException
* @throws JsonProcessingException
* @throws IOException
*/
private String onExecute(Context context, String id, Map<String, String> params)
throws ExecutionException, JsonProcessingException, IOException, ScriptException {
String name = params.get("name");
JsonValue config;
try {
config = connectionFactory.getConnection().read(context, Requests.newReadRequest("config/" + name)).getContent();
} catch (ResourceException e) {
throw new ExecutionException("Error obtaining named config: '" + name + "'", e);
}
JsonValue invokeContext = config.get(INVOKE_CONTEXT);
return startTaskScanJob(context, "REST", name, invokeContext);
}
private String startTaskScanJob(Context context, String invokerName, String scriptName, JsonValue params) throws ExecutionException {
TaskScannerContext taskScannerContext = null;
try {
JsonValue scriptConfig = params.get("task").expect(Map.class).get("script").expect(Map.class);
ScriptEntry script = scriptRegistry.takeScript(scriptConfig);
taskScannerContext = new TaskScannerContext(invokerName, scriptName, params, context, script);
} catch (ScriptException e) {
throw new ExecutionException(e);
}
addTaskScanRun(taskScannerContext);
TaskScannerJob taskScanJob = new TaskScannerJob(connectionFactory, taskScannerContext);
return taskScanJob.startTask();
}
private void addTaskScanRun(TaskScannerContext context) {
// Clean out run history if needed
// Since it only checks for completed runs when a new run is started this
// only provides for approximate adherence to maxCompleteRuns
synchronized(taskScanRuns) {
if (taskScanRuns.size() > maxCompletedRuns) {
int completedCount = 0;
// Since oldest runs are first in the list, inspect backwards
ListIterator<String> iter = new ArrayList<String>(taskScanRuns.keySet())
.listIterator(taskScanRuns.size());
while (iter.hasPrevious()) {
String key = iter.previous();
TaskScannerContext aRun = taskScanRuns.get(key);
if (aRun.isCompleted()) {
++completedCount;
if (completedCount > maxCompletedRuns) {
taskScanRuns.remove(key);
}
}
}
}
taskScanRuns.put(context.getTaskScanID(), context);
}
}
@Override
public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) {
return notSupported(request).asPromise();
}
@Override
public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) {
return notSupported(request).asPromise();
}
@Override
public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) {
return notSupported(request).asPromise();
}
@Override
public Promise<QueryResponse, ResourceException> handleQuery(Context context, QueryRequest request, QueryResourceHandler handler) {
return notSupported(request).asPromise();
}
@Override
public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) {
return notSupported(request).asPromise();
}
}