/**
* Copyright 2008 The University of North Carolina at Chapel Hill
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.unc.lib.dl.cdr.services.rest;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.StAXStreamBuilder;
import org.jdom2.input.stax.DefaultStAXFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.unc.lib.dl.acl.util.AccessGroupConstants;
import edu.unc.lib.dl.acl.util.AccessGroupSet;
import edu.unc.lib.dl.acl.util.GroupsThreadStore;
import edu.unc.lib.dl.util.DepositConstants;
import edu.unc.lib.dl.util.DepositStatusFactory;
import edu.unc.lib.dl.util.JobStatusFactory;
import edu.unc.lib.dl.util.RedisWorkerConstants;
import edu.unc.lib.dl.util.RedisWorkerConstants.DepositAction;
import edu.unc.lib.dl.util.RedisWorkerConstants.DepositField;
import edu.unc.lib.dl.util.RedisWorkerConstants.DepositState;
import edu.unc.lib.dl.xml.JDOMNamespaceUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* @author Gregory Jansen
*
*/
@Controller
@RequestMapping(value = { "/edit/deposit*", "/edit/deposit" })
public class DepositController {
private static final Logger LOG = LoggerFactory
.getLogger(DepositController.class);
public static final String BASE_PATH = "/api/edit/deposit/";
@Resource
protected JedisPool jedisPool;
@Resource
private DepositStatusFactory depositStatusFactory;
@Resource
private JobStatusFactory jobStatusFactory;
@Resource
private File batchIngestFolder;
class MutableInt {
int value = 0;
public void increment () { ++value; }
public int get () { return value; }
}
@PostConstruct
public void init() {
}
@RequestMapping(value = { "", "/" }, method = RequestMethod.GET)
public @ResponseBody
Map<String, ? extends Object> getInfo() {
LOG.debug("getInfo() called");
Map<String, Object> result = new HashMap<String, Object>();
Map<String, MutableInt> counts = countWorkerStates();
Map<String, MutableInt> countDepositStates = countDepositStates();
int active = counts.get("active").get();
result.put("active", (active > 0) ? true : false);
result.put("idle", (counts.get("idle").get()) > 0 ? true : false);
//result.put("activeJobs", counts.get("active").get());
result.put("activeJobs", countDepositStates.get(DepositState.running.name()).get());
result.put("queuedJobs", countDepositStates.get(DepositState.queued.name()).get());
result.put("pausedJobs", countDepositStates.get(DepositState.paused.name()).get());
result.put("failedJobs", countDepositStates.get(DepositState.failed.name()).get());
result.put("finishedJobs", countDepositStates.get(DepositState.finished.name()).get());
result.put("id", "DEPOSIT");
LOG.debug("getInfo() added counts: {}", result);
Map<String, Object> uris = new HashMap<String, Object>();
result.put("uris", uris);
for(DepositState s : DepositState.values()) {
uris.put(s.name(), BASE_PATH + s.name());
}
LOG.debug("getInfo() has: {}", result);
return result;
}
public @ResponseBody
Map<String, MutableInt> countDepositStates() {
Map<String, MutableInt> result = new HashMap<String, MutableInt>();
result.put(DepositState.cancelled.name(), new MutableInt());
result.put(DepositState.failed.name(), new MutableInt());
result.put(DepositState.finished.name(), new MutableInt());
result.put(DepositState.paused.name(), new MutableInt());
result.put(DepositState.queued.name(), new MutableInt());
result.put(DepositState.running.name(), new MutableInt());
result.put(DepositState.unregistered.name(), new MutableInt());
LOG.debug("count deposit states");
Map<String, Map<String, String>> deposits = fetchDepositMap();
for (Map<String, String> deposit : deposits.values()) {
String state = deposit.get(DepositField.state.name());
MutableInt it = result.get(state);
if(it != null) it.increment();
}
return result;
}
private Map<String, MutableInt> countWorkerStates() {
try (Jedis jedis = getJedisPool().getResource()) {
Map<String, MutableInt> result = new HashMap<>();
result.put("idle", new MutableInt());
result.put("paused", new MutableInt());
result.put("active", new MutableInt());
final Set<String> workerNames = jedis.smembers("resque:workers");
for (final String workerName : workerNames) {
final String statusPayload = jedis.get("resque:worker:" + workerName);
if (statusPayload == null) { // no payload key for works that just started
result.get("idle").increment();
} else {
try {
JsonNode w = new ObjectMapper().readTree(statusPayload.getBytes());
if (w.get("paused").asBoolean()) {
result.get("paused").increment();
} else {
result.get("active").increment();
}
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
private Map<String, Map<String, String>> fetchDepositMap() {
Map<String, Map<String, String>> result = new HashMap<>();
for (Map<String, String> deposit : this.depositStatusFactory.getAll()) {
String uuid = deposit.get(DepositField.uuid.name());
result.put(uuid, deposit);
}
return result;
}
public Map<String, Object> getByState(DepositState state) {
LOG.debug("get by state: {}", state.name());
Map<String, Object> result = new HashMap<>();
Map<String, Map<String, String>> deposits = fetchDepositMap();
boolean isRunning = state.equals(DepositState.running);
for (Map<String, String> deposit : deposits.values()) {
if (state.name().equals(deposit.get(DepositField.state.name()))) {
Map<String, Object> depositResult = new HashMap<String, Object>();
String uuid = deposit.get(DepositField.uuid.name());
for (Entry<String, String> field : deposit.entrySet()) {
depositResult.put(field.getKey(), field.getValue());
}
if (isRunning) {
String jobUUID = jobStatusFactory.getWorkingJob(uuid);
if (jobUUID != null) {
Map<String, String> jobStatus = jobStatusFactory.get(jobUUID);
depositResult.put("currentJob", jobStatus);
}
}
result.put(uuid, depositResult);
}
}
return result;
}
private Map<String, Object> getDetails(String depositUUID) {
Map<String, String> status = depositStatusFactory.get(depositUUID);
Map<String, Object> result = new HashMap<String, Object>();
for (Entry<String, String> field : status.entrySet()) {
result.put(field.getKey(), field.getValue());
}
Map<String, Map<String, String>> jobStatuses = jobStatusFactory.getAllJobs(depositUUID);
result.put("jobs", jobStatuses);
String state = status.get(DepositField.state.name());
if (DepositState.running.name().equals(state)) {
String jobUUID = jobStatusFactory.getWorkingJob(depositUUID);
result.put("currentJobUUID", jobUUID);
}
result.put("jobsURI", "/api/status/deposit/" + depositUUID + "/jobs");
result.put("eventsURI", "/api/status/deposit/" + depositUUID + "/eventsXML");
return result;
}
@RequestMapping(value = { "{stateOrUUID}", "/{stateOrUUID}" }, method = RequestMethod.GET)
public @ResponseBody
Map<String, Object> get(@PathVariable String stateOrUUID) {
DepositState depositState = null;
try {
depositState = DepositState.valueOf(stateOrUUID);
} catch(IllegalArgumentException ignore) {}
// Request was for a state, get all jobs in that state
if (depositState != null) {
return getByState(depositState);
} else {
// Request was for a specific job
return getDetails(stateOrUUID);
}
}
/**
* Aborts the deposit, reversing any ingests and scheduling a cleanup job.
*
* @param depositUUID
*/
@RequestMapping(value = { "{uuid}", "/{uuid}" }, method = RequestMethod.DELETE)
public void destroy(@PathVariable String uuid) {
// verify deposit is registered and not yet cleaned up
// set deposit status to canceling
}
/**
* Request to pause, resume, cancel or destroy a deposit. The deposit cancel action will stop the deposit, purge any
* ingested objects and schedule deposit destroy in the future. The deposit pause action halts work on a deposit such
* that it can be resumed later. The deposit destroy action cleans up the submitted deposit package, leaving staged
* files alone.
*
* @param depositUUID
* the unique identifier of the deposit
* @param action
* the action to take on the deposit (pause, resume, cancel, destroy)
*/
@RequestMapping(value = { "{uuid}", "/{uuid}" }, method = RequestMethod.POST)
public void update(@PathVariable String uuid, @RequestParam(required = true) String action,
HttpServletResponse response) {
DepositAction actionRequested = DepositAction.valueOf(action);
if (actionRequested == null) {
throw new IllegalArgumentException(
"The deposit action is not recognized: " + action);
}
// permission check, admin group or depositor required
AccessGroupSet groups = GroupsThreadStore.getGroups();
String username = GroupsThreadStore.getUsername();
Map<String, String> status = depositStatusFactory.get(uuid);
if(!groups.contains(AccessGroupConstants.ADMIN_GROUP)) {
if(username == null || !username.equals(status.get(DepositField.depositorName))) {
response.setStatus(403);
return;
}
}
String state = status.get(DepositField.state.name());
switch (actionRequested) {
case pause:
if (DepositState.finished.name().equals(state)) {
throw new IllegalArgumentException("That deposit has already finished");
} else if (DepositState.failed.name().equals(state)) {
throw new IllegalArgumentException("That deposit has already failed");
} else {
depositStatusFactory.requestAction(uuid, DepositAction.pause);
response.setStatus(204);
}
break;
case resume:
if (!DepositState.paused.name().equals(state) && !DepositState.failed.name().equals(state)) {
throw new IllegalArgumentException("The deposit must be paused or failed before you can resume");
} else {
depositStatusFactory.requestAction(uuid, DepositAction.resume);
response.setStatus(204);
}
break;
case cancel:
if (DepositState.finished.name().equals(state)) {
throw new IllegalArgumentException("That deposit has already finished");
} else {
depositStatusFactory.requestAction(uuid, DepositAction.cancel);
response.setStatus(204);
}
break;
case destroy:
if (DepositState.cancelled.name().equals(state) || DepositState.finished.name().equals(state)) {
depositStatusFactory.requestAction(uuid, DepositAction.destroy);
response.setStatus(204);
} else {
throw new IllegalArgumentException("The deposit must be finished or cancelled before it is destroyed");
}
break;
default:
throw new IllegalArgumentException("The requested deposit action is not implemented: " + action);
}
}
@RequestMapping(value = { "{uuid}/jobs", "/{uuid}/jobs" }, method = RequestMethod.GET)
public @ResponseBody
Map<String, Map<String, String>> getJobs(@PathVariable String uuid) {
LOG.debug("getJobs( {} )", uuid);
try (Jedis jedis = getJedisPool().getResource()) {
Map<String, Map<String, String>> jobs = new HashMap<>();
Set<String> jobUUIDs = jedis
.smembers(RedisWorkerConstants.DEPOSIT_TO_JOBS_PREFIX + uuid);
for (String jobUUID : jobUUIDs) {
Map<String, String> info = jedis
.hgetAll(RedisWorkerConstants.JOB_STATUS_PREFIX + jobUUID);
jobs.put(jobUUID, info);
}
return jobs;
}
}
public JedisPool getJedisPool() {
return jedisPool;
}
@RequestMapping(value = { "{uuid}/events" }, method = RequestMethod.GET)
public @ResponseBody
Document getEvents(@PathVariable String uuid) throws Exception {
LOG.debug("getEvents( {} )", uuid);
String bagDirectory;
try (Jedis jedis = getJedisPool().getResource()) {
bagDirectory = jedis.hget(
RedisWorkerConstants.DEPOSIT_STATUS_PREFIX + uuid,
RedisWorkerConstants.DepositField.directory.name());
}
File bagFile = new File(bagDirectory);
if (!bagFile.exists())
return null;
File eventsFile = new File(bagDirectory, DepositConstants.EVENTS_FILE);
if (!eventsFile.exists())
return null;
Element events = new Element("events", JDOMNamespaceUtil.PREMIS_V2_NS);
Document result = new Document(events);
XMLInputFactory factory = XMLInputFactory.newInstance();
try(FileInputStream fis = new FileInputStream(eventsFile)) {
XMLStreamReader reader = factory.createXMLStreamReader(fis);
StAXStreamBuilder builder = new StAXStreamBuilder();
List<Content> list = builder.buildFragments(reader,
new DefaultStAXFilter());
events.addContent(list);
return result;
}
}
}