/*
* Copyright 2017
* Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology
* Technische Universität Darmstadt
*
* 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 de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi;
import static de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil.isProjectAdmin;
import static de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil.isProjectCreator;
import static de.tudarmstadt.ukp.clarin.webanno.api.SecurityUtil.isSuperAdmin;
import static org.apache.uima.fit.util.JCasUtil.select;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Resource;
import javax.persistence.NoResultException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.uima.analysis_component.JCasAnnotator_ImplBase;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.Feature;
import org.apache.uima.cas.FeatureStructure;
import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.impl.FeatureImpl;
import org.apache.uima.cas.impl.TypeImpl;
import org.apache.uima.cas.impl.TypeSystemImpl;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.collection.CollectionReader;
import org.apache.uima.fit.util.FSUtil;
import org.apache.uima.jcas.JCas;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.multipart.MultipartFile;
import org.springframework.web.util.UriComponentsBuilder;
import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService;
import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService;
import de.tudarmstadt.ukp.clarin.webanno.api.ImportExportService;
import de.tudarmstadt.ukp.clarin.webanno.api.ProjectService;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocument;
import de.tudarmstadt.ukp.clarin.webanno.model.Mode;
import de.tudarmstadt.ukp.clarin.webanno.model.PermissionLevel;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
import de.tudarmstadt.ukp.clarin.webanno.model.ProjectPermission;
import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.clarin.webanno.security.UserDao;
import de.tudarmstadt.ukp.clarin.webanno.security.model.User;
import de.tudarmstadt.ukp.clarin.webanno.tsv.WebannoTsv3Writer;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.AccessForbiddenException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.IncompatibleDocumentException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.ObjectExistsException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.ObjectNotFoundException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.RemoteApiException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.exception.UnsupportedFormatException;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.model.RAnnotation;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.model.RDocument;
import de.tudarmstadt.ukp.clarin.webanno.webapp.remoteapi.v2.model.RProject;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence;
import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Info;
import io.swagger.annotations.SwaggerDefinition;
@SwaggerDefinition(info=@Info(title="WebAnno Remote API", version="2"))
@RequestMapping(RemoteApiController2.API_BASE)
@Controller
public class RemoteApiController2
{
public static final String API_BASE = "/api/v2";
private static final String PROJECTS = "projects";
private static final String DOCUMENTS = "documents";
private static final String ANNOTATIONS = "annotations";
private static final String PARAM_FILE = "file";
private static final String PARAM_NAME = "name";
private static final String PARAM_FORMAT = "format";
private static final String PARAM_CREATOR = "creator";
private static final String PARAM_PROJECT_ID = "projectId";
private static final String PARAM_ANNOTATOR_ID = "userId";
private static final String PARAM_DOCUMENT_ID = "documentId";
private static final String VAL_ORIGINAL = "ORIGINAL";
private static final String PROP_ID = "id";
private static final String PROP_NAME = "name";
private static final String PROP_STATE = "state";
private static final String PROP_USER = "user";
private static final String PROP_TIMESTAMP = "user";
private static final String FORMAT_DEFAULT = "text";
private final Logger LOG = LoggerFactory.getLogger(getClass());
@Resource(name = "documentService")
private DocumentService documentService;
@Resource(name = "projectService")
private ProjectService projectService;
@Resource(name = "importExportService")
private ImportExportService importExportService;
@Resource(name = "annotationService")
private AnnotationSchemaService annotationService;
@Resource(name = "userRepository")
private UserDao userRepository;
@ExceptionHandler(value = RemoteApiException.class)
public void handleException(RemoteApiException aException, HttpServletResponse aResponse)
throws IOException
{
LOG.error(aException.getMessage(), aException);
aResponse.setStatus(aException.getStatus().value());
try (PrintWriter out = aResponse.getWriter()) {
out.print(aException.getMessage());
}
}
@ExceptionHandler
public void handleException(Exception aException, HttpServletResponse aResponse)
throws IOException
{
LOG.error(aException.getMessage(), aException);
aResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
try (PrintWriter out = aResponse.getWriter()) {
out.print("Internal server error: ");
out.print(aException.getMessage());
}
}
private User getCurrentUser()
throws ObjectNotFoundException
{
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return getUser(username);
}
private User getUser(String aUserId)
throws ObjectNotFoundException
{
User user = userRepository.get(aUserId);
if (user == null) {
throw new ObjectNotFoundException("User [" + aUserId + "] not found.");
}
return user;
}
private Project getProject(long aProjectId)
throws ObjectNotFoundException, AccessForbiddenException
{
// Get current user - this will throw an exception if the current user does not exit
User user = getCurrentUser();
// Get project
Project project;
try {
project = projectService.getProject(aProjectId);
}
catch (NoResultException e) {
throw new ObjectNotFoundException("Project [" + aProjectId + "] not found.");
}
// Check for the access
assertPermission(
"User [" + user.getUsername() + "] is not allowed to access project [" + aProjectId
+ "]",
isProjectAdmin(project, projectService, user)
|| isSuperAdmin(projectService, user));
return project;
}
private SourceDocument getDocument(Project aProject, long aDocumentId)
throws ObjectNotFoundException
{
try {
return documentService.getSourceDocument(aProject.getId(), aDocumentId);
}
catch (NoResultException e) {
throw new ObjectNotFoundException(
"Document [" + aDocumentId + "] in project [" + aProject.getId() + "] not found.");
}
}
private AnnotationDocument getAnnotation(SourceDocument aDocument, String aUser,
boolean aCreateIfMissing)
throws ObjectNotFoundException
{
try {
if (aCreateIfMissing) {
return documentService.createOrGetAnnotationDocument(aDocument, getUser(aUser));
}
else {
return documentService.getAnnotationDocument(aDocument, getUser(aUser));
}
}
catch (NoResultException e) {
throw new ObjectNotFoundException(
"Annotation for user [" + aUser + "] on document [" + aDocument.getId()
+ "] in project [" + aDocument.getProject().getId() + "] not found.");
}
}
private void assertPermission(String aMessage, boolean aHasAccess)
throws AccessForbiddenException
{
if (!aHasAccess) {
throw new AccessForbiddenException(aMessage);
}
}
@ApiOperation(value = "List the projects accessible by the authenticated user")
@RequestMapping(
value = ("/" + PROJECTS),
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<List<RProject>> projectList()
throws Exception
{
// Get current user - this will throw an exception if the current user does not exit
User user = getCurrentUser();
// Get projects with permission
List<Project> accessibleProjects = projectService.listAccessibleProjects(user);
// Collect all the projects
List<RProject> projectList = new ArrayList<>();
for (Project project : accessibleProjects) {
projectList.add(new RProject(project));
}
return ResponseEntity.ok(projectList);
}
@ApiOperation(value = "Create a new project")
@RequestMapping(
value = ("/" + PROJECTS),
method = RequestMethod.POST,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<RProject> projectCreate(
@RequestParam(PARAM_NAME) String aName,
@RequestParam(PARAM_CREATOR) Optional<String> aCreator,
UriComponentsBuilder aUcb)
throws Exception
{
// Get current user - this will throw an exception if the current user does not exit
User user = getCurrentUser();
// Check for the access
assertPermission("User [" + user.getUsername() + "] is not allowed to create projects",
isProjectCreator(projectService, user) || isSuperAdmin(projectService, user));
// Check if the user can create projects for another user
assertPermission(
"User [" + user.getUsername() + "] is not allowed to create projects for user ["
+ aCreator.orElse("<unspecified>") + "]",
isSuperAdmin(projectService, user)
|| (aCreator.isPresent() && aCreator.equals(user.getUsername())));
// Existing project
if (projectService.existsProject(aName)) {
throw new ObjectExistsException("A project with name [" + aName + "] already exists");
}
// Create the project and initialize tags
LOG.info("Creating project [" + aName + "]");
Project project = new Project();
project.setName(aName);
projectService.createProject(project);
annotationService.initializeTypesForProject(project);
// Create permission for the project creator
String owner = aCreator.isPresent() ? aCreator.get() : user.getUsername();
projectService.createProjectPermission(
new ProjectPermission(project, owner, PermissionLevel.ADMIN));
projectService.createProjectPermission(
new ProjectPermission(project, owner, PermissionLevel.CURATOR));
projectService.createProjectPermission(
new ProjectPermission(project, owner, PermissionLevel.USER));
RProject response = new RProject(project);
return ResponseEntity.created(aUcb.path(API_BASE + "/" + PROJECTS + "/{id}")
.buildAndExpand(project.getId()).toUri()).body(response);
}
@ApiOperation(value = "Get information about a project")
@RequestMapping(
value = ("/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}"),
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<RProject> projectRead(
@PathVariable(PARAM_PROJECT_ID) long aProjectId)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
RProject response = new RProject(project);
return ResponseEntity.ok(response);
}
@ApiOperation(value = "Delete an existing project")
@RequestMapping(
value = ("/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}"),
method = RequestMethod.DELETE,
produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> projectDelete(
@PathVariable(PARAM_PROJECT_ID) long aProjectId)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
projectService.removeProject(project);
return ResponseEntity.ok("Project [" + aProjectId + "] deleted.");
}
@ApiOperation(value = "List documents in a project")
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<List<RDocument>> documentList(
@PathVariable(PARAM_PROJECT_ID) long aProjectId)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
List<SourceDocument> documents = documentService.listSourceDocuments(project);
List<RDocument> documentList = new ArrayList<>();
for (SourceDocument document : documents) {
documentList.add(new RDocument(document));
}
return ResponseEntity.ok(documentList);
}
@ApiOperation(value = "Create a new document in a project")
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS,
method = RequestMethod.POST,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<RDocument> documentCreate(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@RequestParam(value = PARAM_FILE) MultipartFile aFile,
@RequestParam(value = PARAM_NAME) String aName,
@RequestParam(value = PARAM_FORMAT) String aFormat,
UriComponentsBuilder aUcb)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
// Check if the format is supported
Map<String, Class<CollectionReader>> readableFormats = importExportService
.getReadableFormats();
if (readableFormats.get(aFormat) == null) {
throw new UnsupportedFormatException(
"Format [%s] not supported. Acceptable formats are %s.", aFormat,
readableFormats.keySet());
}
// Meta data entry to the database
SourceDocument document = new SourceDocument();
document.setProject(project);
document.setName(aName);
document.setFormat(aFormat);
documentService.createSourceDocument(document);
// Import source document to the project repository folder
try (InputStream is = aFile.getInputStream()) {
documentService.uploadSourceDocument(is, document);
}
RDocument rDocument = new RDocument(document);
return ResponseEntity
.created(aUcb.path(API_BASE + "/" + PROJECTS + "/{pid}/" + DOCUMENTS + "/{did}")
.buildAndExpand(project.getId(), document.getId()).toUri())
.body(rDocument);
}
@ApiOperation(value = "Get a document from a project", response=byte[].class)
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS + "/{"
+ PARAM_DOCUMENT_ID + "}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity documentRead(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@PathVariable(PARAM_DOCUMENT_ID) long aDocumentId,
@RequestParam(value = PARAM_FORMAT) Optional<String> aFormat)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
SourceDocument doc = getDocument(project, aDocumentId);
boolean originalFile;
String format;
if (aFormat.isPresent()) {
if (VAL_ORIGINAL.equals(aFormat.get())) {
format = doc.getFormat();
originalFile = true;
}
else {
format = aFormat.get();
originalFile = doc.getFormat().equals(format);
}
}
else {
format = doc.getFormat();
originalFile = true;
}
if (originalFile) {
// Export the original file - no temporary file created here, we export directly from
// the file system
File docFile = documentService.getSourceDocumentFile(doc);
FileSystemResource resource = new FileSystemResource(docFile);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(resource.contentLength());
httpHeaders.set("Content-Disposition", "attachment; filename=\""+doc.getName()+"\"");
return new ResponseEntity<org.springframework.core.io.Resource>(resource, httpHeaders,
HttpStatus.OK);
}
else {
// Export a converted file - here we first export to a local temporary file and then
// send that back to the client
// Check if the format is supported
Map<String, Class<JCasAnnotator_ImplBase>> writableFormats = importExportService
.getWritableFormats();
Class<JCasAnnotator_ImplBase> writer = writableFormats.get(format);
if (writer == null) {
throw new UnsupportedFormatException(
"Format [%s] cannot be exported. Exportable formats are %s.", aFormat,
writableFormats.keySet());
}
// Create a temporary export file from the annotations
JCas jcas = documentService.createOrReadInitialCas(doc);
File exportedFile = null;
try {
// Load the converted file into memory
exportedFile = importExportService.exportCasToFile(jcas.getCas(), doc,
doc.getName(), writer, true);
byte[] resource = FileUtils.readFileToByteArray(exportedFile);
// Send it back to the client
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(resource.length);
httpHeaders.set("Content-Disposition", "attachment; filename=\""+exportedFile.getName()+"\"");
return new ResponseEntity<byte[]>(resource, httpHeaders, HttpStatus.OK);
}
finally {
if (exportedFile != null) {
FileUtils.forceDelete(exportedFile);
}
}
}
}
@ApiOperation(value = "Delete a document from a project")
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS + "/{"
+ PARAM_DOCUMENT_ID + "}/",
method = RequestMethod.DELETE,
produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> documentDelete(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@PathVariable(PARAM_DOCUMENT_ID) long aDocumentId)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
SourceDocument doc = getDocument(project, aDocumentId);
documentService.removeSourceDocument(doc);
return ResponseEntity
.ok("Document [" + aDocumentId + "] deleted from project [" + aProjectId + "].");
}
@ApiOperation(value = "List annotations of a document in a project")
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS + "/{"
+ PARAM_DOCUMENT_ID + "}/" + ANNOTATIONS,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<List<RAnnotation>> annotationsList(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@PathVariable(PARAM_DOCUMENT_ID) long aDocumentId)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
SourceDocument doc = getDocument(project, aDocumentId);
List<AnnotationDocument> annotations = documentService.listAnnotationDocuments(doc);
List<RAnnotation> annotationList = new ArrayList<>();
for (AnnotationDocument annotation : annotations) {
annotationList.add(new RAnnotation(annotation));
}
return ResponseEntity.ok(annotationList);
}
@ApiOperation(value = "Create annotations for a document in a project")
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS + "/{"
+ PARAM_DOCUMENT_ID + "}/" + ANNOTATIONS + "/{" + PARAM_ANNOTATOR_ID + "}",
method = RequestMethod.POST,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<RAnnotation> annotationsCreate(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@PathVariable(PARAM_DOCUMENT_ID) long aDocumentId,
@PathVariable(PARAM_ANNOTATOR_ID) String aAnnotatorId,
@RequestParam(value = PARAM_FILE) MultipartFile aFile,
@RequestParam(value = PARAM_FORMAT) Optional<String> aFormat,
UriComponentsBuilder aUcb)
throws Exception
{
User annotator = getUser(aAnnotatorId);
Project project = getProject(aProjectId);
SourceDocument document = getDocument(project, aDocumentId);
AnnotationDocument anno = getAnnotation(document, aAnnotatorId, true);
// Check if the format is supported
String format = aFormat.orElse(FORMAT_DEFAULT);
Map<String, Class<CollectionReader>> readableFormats = importExportService
.getReadableFormats();
if (readableFormats.get(format) == null) {
throw new UnsupportedFormatException(
"Format [%s] not supported. Acceptable formats are %s.", format,
readableFormats.keySet());
}
// Convert the uploaded annotation document into a CAS
File tmpFile = null;
JCas annotationCas;
try {
tmpFile = File.createTempFile("upload", ".bin");
aFile.transferTo(tmpFile);
annotationCas = importExportService.importCasFromFile(tmpFile, project, format);
}
finally {
if (tmpFile != null) {
FileUtils.forceDelete(tmpFile);
}
}
// Check if the uploaded file is compatible with the source document. They are compatible
// if the text is the same and if all the token and sentence annotations have the same
// offsets.
JCas initialCas = documentService.createOrReadInitialCas(document);
String initialText = initialCas.getDocumentText();
String annotationText = annotationCas.getDocumentText();
// If any of the texts contains tailing line breaks, we ignore that. We assume at the moment
// that nobody will have created annotations over that trailing line breaks.
initialText = StringUtils.chomp(initialText);
annotationText = StringUtils.chomp(annotationText);
if (ObjectUtils.notEqual(initialText, annotationText)) {
int diffIndex = StringUtils.indexOfDifference(initialText, annotationText);
String expected = initialText.substring(diffIndex,
Math.min(initialText.length(), diffIndex + 20));
String actual = annotationText.substring(diffIndex,
Math.min(annotationText.length(), diffIndex + 20));
throw new IncompatibleDocumentException(
"Text of annotation document does not match text of source document at offset "
+ "[%d]. Expected [%s] but found [%s].",
diffIndex, expected, actual);
}
// Just in case we really had to chomp off a trailing line break from the annotation CAS,
// make sure we copy over the proper text from the initial CAS
// NOT AT HOME THIS YOU SHOULD TRY
// SETTING THE SOFA STRING FORCEFULLY FOLLOWING THE DARK SIDE IS!
forceSetFeatureValue(annotationCas.getSofa(), CAS.FEATURE_BASE_NAME_SOFASTRING,
initialCas.getDocumentText());
FSUtil.setFeature(annotationCas.getDocumentAnnotationFs(), CAS.FEATURE_BASE_NAME_END,
initialCas.getDocumentText().length());
Collection<Sentence> annotationSentences = select(annotationCas, Sentence.class);
Collection<Sentence> initialSentences = select(initialCas, Sentence.class);
if (annotationSentences.size() != initialSentences.size()) {
throw new IncompatibleDocumentException(
"Expected [%d] sentences, but annotation document contains [%d] sentences.",
initialSentences.size(), annotationSentences.size());
}
assertCompatibleOffsets(initialSentences, annotationSentences);
Collection<Token> annotationTokens = select(annotationCas, Token.class);
Collection<Token> initialTokens = select(initialCas, Token.class);
if (annotationTokens.size() != initialTokens.size()) {
throw new IncompatibleDocumentException(
"Expected [%d] sentences, but annotation document contains [%d] sentences.",
initialSentences.size(), annotationSentences.size());
}
assertCompatibleOffsets(initialTokens, annotationTokens);
// If they are compatible, then we can store the new annotations
documentService.writeAnnotationCas(annotationCas, document, annotator, false);
RAnnotation response = new RAnnotation(anno);
return ResponseEntity.created(aUcb
.path(API_BASE + "/" + PROJECTS + "/{pid}/" + DOCUMENTS + "/{did}/" + ANNOTATIONS
+ "{aid}")
.buildAndExpand(project.getId(), document.getId(), annotator.getUsername()).toUri())
.body(response);
}
@ApiOperation(value = "Get annotations of a document in a project", response=byte[].class)
@RequestMapping(
value = "/" + PROJECTS + "/{" + PARAM_PROJECT_ID + "}/" + DOCUMENTS + "/{"
+ PARAM_DOCUMENT_ID + "}/" + ANNOTATIONS + "/{" + PARAM_ANNOTATOR_ID + "}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> annotationsRead(
@PathVariable(PARAM_PROJECT_ID) long aProjectId,
@PathVariable(PARAM_DOCUMENT_ID) long aDocumentId,
@PathVariable(PARAM_ANNOTATOR_ID) String aAnnotatorId,
@RequestParam(value = PARAM_FORMAT) Optional<String> aFormat)
throws Exception
{
// Get project (this also ensures that it exists and that the current user can access it
Project project = getProject(aProjectId);
SourceDocument doc = getDocument(project, aDocumentId);
// Check format
String format;
if (aFormat.isPresent()) {
if (VAL_ORIGINAL.equals(aFormat.get())) {
format = doc.getFormat();
}
else {
format = aFormat.get();
}
}
else {
format = doc.getFormat();
}
// Determine the format
Class<?> writer = importExportService.getWritableFormats().get(format);
if (writer == null) {
String msg = "[" + doc.getName() + "] No writer found for format [" + format
+ "] - exporting as WebAnno TSV instead.";
LOG.info(msg);
writer = WebannoTsv3Writer.class;
}
// In principle we don't need this call - but it makes sure that we check that the
// annotation document entry is actually properly set up in the database.
AnnotationDocument anno = getAnnotation(doc, aAnnotatorId, false);
// Create a temporary export file from the annotations
File exportedAnnoFile = null;
byte[] resource;
try {
exportedAnnoFile = importExportService.exportAnnotationDocument(doc, anno.getUser(),
writer, doc.getName(), Mode.ANNOTATION);
resource = FileUtils.readFileToByteArray(exportedAnnoFile);
}
finally {
if (exportedAnnoFile != null) {
FileUtils.forceDelete(exportedAnnoFile);
}
}
String filename = FilenameUtils.removeExtension(doc.getName());
filename += "-" + anno.getUser();
filename += "." + FilenameUtils.getExtension(doc.getName());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(resource.length);
httpHeaders.set("Content-Disposition", "attachment; filename=\""+filename+"\"");
return new ResponseEntity<byte[]>(resource, httpHeaders, HttpStatus.OK);
}
private static <T extends AnnotationFS> void assertCompatibleOffsets(Collection<T> aExpected,
Collection<T> aActual)
throws IncompatibleDocumentException
{
int unitIndex = 0;
Iterator<T> asi = aExpected.iterator();
Iterator<T> isi = aActual.iterator();
// At this point we know that the number of sentences is the same, so it is ok to check only
// one of the iterators for hasNext()
while (asi.hasNext()) {
T as = asi.next();
T is = isi.next();
if (as.getBegin() != is.getBegin() || as.getEnd() != is.getEnd()) {
throw new IncompatibleDocumentException(
"Expected %s [%d] to have range [%d-%d], but instead found range "
+ "[%d-%d] in annotation document.",
is.getType().getShortName(), unitIndex, is.getBegin(), is.getEnd(),
as.getBegin(), as.getEnd());
}
unitIndex++;
}
}
private static void forceSetFeatureValue(FeatureStructure aFS, String aFeatureName,
String aValue)
{
CASImpl casImpl = (CASImpl) aFS.getCAS().getLowLevelCAS();
TypeSystemImpl ts = (TypeSystemImpl) aFS.getCAS().getTypeSystem();
Feature feat = aFS.getType().getFeatureByBaseName(aFeatureName);
int featCode = ((FeatureImpl) feat).getCode();
int thisType = ((TypeImpl) aFS.getType()).getCode();
if (!ts.isApprop(thisType, featCode)) {
throw new IllegalArgumentException("Feature structure does not have that feature");
}
if (!ts.subsumes(ts.getType(CAS.TYPE_NAME_STRING), feat.getRange())) {
throw new IllegalArgumentException("Not a string feature!");
}
casImpl.ll_setStringValue(casImpl.ll_getFSRef(aFS), featCode, aValue);
}
}