package nl.knaw.huygens.alexandria.endpoint.iiif;
/*
* #%L
* alexandria-main
* =======
* Copyright (C) 2015 - 2017 Huygens ING (KNAW)
* =======
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.github.jsonldjava.utils.JsonUtils;
import com.google.common.collect.Maps;
import nl.knaw.huygens.alexandria.api.EndpointPaths;
import nl.knaw.huygens.alexandria.api.model.ProcessStatusMap;
import nl.knaw.huygens.alexandria.api.model.iiif.IIIFAnnotationList;
import nl.knaw.huygens.alexandria.config.AlexandriaConfiguration;
import nl.knaw.huygens.alexandria.endpoint.webannotation.WebAnnotation;
import nl.knaw.huygens.alexandria.endpoint.webannotation.WebAnnotationService;
import nl.knaw.huygens.alexandria.exception.BadRequestException;
import nl.knaw.huygens.alexandria.exception.NotFoundException;
import nl.knaw.huygens.alexandria.service.AlexandriaService;
import org.glassfish.jersey.server.ChunkedOutput;
import javax.inject.Singleton;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import static nl.knaw.huygens.alexandria.api.w3c.WebAnnotationConstants.JSONLD_MEDIATYPE;
public class IIIFAnnotationListEndpoint extends AbstractIIIFEndpoint {
private final String listId;
private String name;
private AlexandriaConfiguration config;
private String identifier;
private WebAnnotationService webAnnotationService;
private final ProcessStatusMap<AnnotationListImportStatus> taskStatusMap;
private final ExecutorService executorService;
public IIIFAnnotationListEndpoint(String identifier,
String name,
AlexandriaService service,
AlexandriaConfiguration config,
URI id,
ProcessStatusMap<AnnotationListImportStatus> taskStatusMap,
ExecutorService executorService) {
super(id);
this.identifier = identifier;
this.name = name;
this.config = config;
this.taskStatusMap = taskStatusMap;
this.executorService = executorService;
this.webAnnotationService = new WebAnnotationService(service, config);
this.listId = identifier + ":" + name;
}
@GET
public Response get() {
return notImplemented(dummySequence());
}
@POST
// @Path("streaming")
@Consumes(JSONLD_MEDIATYPE)
public Response postAnnotationListStreaming(InputStream inputStream) throws JsonParseException, JsonMappingException, IOException {
StreamingOutput outStream = os -> {
JsonFactory jsonFactory = new JsonFactory();
JsonParser jParser = jsonFactory.createParser(inputStream);
JsonGenerator jGenerator = jsonFactory.createGenerator(os);
String context = null;
jGenerator.writeStartObject();
while (jParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jParser.getCurrentName();
// Log.info("fieldname={}", fieldname);
if ("@context".equals(fieldname)) {
jParser.nextToken();
context = jParser.getText();
jGenerator.writeFieldName("@context");
jGenerator.writeString(context);
} else if ("resources".equals(fieldname)) {
if (context == null) {
throw new BadRequestException("Missing @context field, should be defined at the start of the json payload.");
}
parseResources(jParser, jGenerator, context);
} else if (fieldname != null) {
jParser.nextToken();
jGenerator.writeFieldName(fieldname);
JsonToken currentToken = jParser.currentToken();
if (currentToken.isStructStart()) {
jGenerator.writeRaw(new ObjectMapper().readTree(jParser).toString());
} else if (currentToken.isNumeric()) {
jGenerator.writeNumber(jParser.getValueAsString());
} else {
jGenerator.writeString(jParser.getValueAsString());
}
}
}
jParser.close();
jGenerator.writeEndObject();
jGenerator.flush();
jGenerator.close();
};
return Response.ok(outStream).build();
}
@POST
@Path("chunked")
@Consumes(JSONLD_MEDIATYPE)
public ChunkedOutput<String> postAnnotationListChunked(InputStream inputStream) throws JsonParseException, JsonMappingException, IOException {
final ChunkedOutput<String> output = new ChunkedOutput<>(String.class);
AnnotationListHandler alh = new AnnotationListHandler(inputStream, webAnnotationService);
new Thread() {
@Override
public void run() {
try {
String chunk;
while ((chunk = alh.getNextString()) != null) {
output.write(chunk);
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
return output;
}
@POST
@Path("oldway")
@Consumes(JSONLD_MEDIATYPE)
public Response postAnnotationList(IIIFAnnotationList annotationList) {
Map<String, Object> otherProperties = annotationList.getOtherProperties();
String context = annotationList.getContext();
Map<String, Object> processedList = Maps.newHashMap(otherProperties);
List<Map<String, Object>> resources = new ArrayList<>(annotationList.getResources().size());
annotationList.getResources().forEach(prototype -> {
prototype.setCreated(Instant.now().toString());
prototype.getVariablePart().put("@context", context);
WebAnnotation webAnnotation = webAnnotationService.validateAndStore(prototype);
try {
Map<String, Object> map = (Map<String, Object>) JsonUtils.fromString(webAnnotation.json());
resources.add(map);
} catch (IOException e) {
e.printStackTrace();
}
});
processedList.put("resources", resources);
return ok(processedList);
}
@POST
@Path("async")
@Consumes(JSONLD_MEDIATYPE)
public Response postAnnotationListAsynchronously(IIIFAnnotationList annotationList) {
UUID statusId = startAnnotationListImport(annotationList);
URI statusURI = UriBuilder.fromUri(config.getBaseURI())
.path(EndpointPaths.IIIF)
.path(identifier)
.path("list")
.path(name)
.path("async")
.path("status")
.path(statusId.toString())
.build();
return Response.accepted()//
.location(statusURI)//
.build();
}
private UUID startAnnotationListImport(IIIFAnnotationList annotationList) {
AnnotationListImportTask task = new AnnotationListImportTask(annotationList, webAnnotationService);
UUID statusUUID = UUID.randomUUID();
taskStatusMap.put(statusUUID, task.getStatus());
runTask(task);
return statusUUID;
}
private void runTask(Runnable task) {
if (config.asynchronousEndpointsAllowed()) {
executorService.execute(task);
} else {
// For now, for the acceptance tests.
task.run();
}
}
@GET
@Path("async/status/{uuid}")
public Response getAnnotationListImportStatus(@PathParam("uuid") UUID uuid) {
AnnotationListImportStatus annotationListImportTaskStatus = taskStatusMap.get(uuid)//
.orElseThrow(NotFoundException::new);
return ok(annotationListImportTaskStatus);
}
private void parseResources(JsonParser jParser, JsonGenerator jGenerator, String context) throws IOException {
jGenerator.writeFieldName("resources");
jParser.nextToken(); // "["
jGenerator.writeStartArray();
// parse each resource
ObjectMapper mapper = new ObjectMapper();
boolean first = true;
while (jParser.nextToken() != JsonToken.END_ARRAY) {
if (!first) {
jGenerator.writeRaw(",");
}
ObjectNode annotationNode = mapper.readTree(jParser);
String created = Instant.now().toString();
annotationNode.set("http://purl.org/dc/terms/created", new TextNode(created));
annotationNode.set("@context", new TextNode(context));
ObjectNode storedAnnotation = webAnnotationService.validateAndStore(annotationNode);
jGenerator.writeRaw(new ObjectMapper().writeValueAsString(storedAnnotation));
first = false;
}
jGenerator.writeEndArray();
}
private Map<String, Object> dummySequence() {
Map<String, Object> dummy = baseMap();
return dummy;
}
@Override
String getType() {
return "sc:AnnotationList";
}
}