/**
* PODD is an OWL ontology database used for scientific project management
*
* Copyright (C) 2009-2013 The University Of Queensland
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero 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
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.podd.resources;
import info.aduna.iteration.Iterations;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.openrdf.OpenRDFException;
import org.openrdf.model.Model;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.impl.LinkedHashModel;
import org.openrdf.model.vocabulary.OWL;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.RDFWriter;
import org.openrdf.rio.Rio;
import org.openrdf.rio.UnsupportedRDFormatException;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.ext.fileupload.RestletFileUpload;
import org.restlet.representation.ByteArrayRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.representation.Variant;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.ResourceException;
import org.restlet.security.User;
import org.semanticweb.owlapi.model.OWLException;
import org.semanticweb.owlapi.model.OWLOntologyID;
import com.github.podd.api.DanglingObjectPolicy;
import com.github.podd.api.DataReferenceVerificationPolicy;
import com.github.podd.api.PoddArtifactManager;
import com.github.podd.exception.DuplicateArtifactIRIException;
import com.github.podd.exception.PoddException;
import com.github.podd.exception.RepositoryNotFoundException;
import com.github.podd.exception.SchemaManifestException;
import com.github.podd.exception.UnmanagedArtifactIRIException;
import com.github.podd.exception.UnmanagedArtifactVersionException;
import com.github.podd.exception.UnmanagedSchemaIRIException;
import com.github.podd.restlet.PoddAction;
import com.github.podd.restlet.PoddSesameRealm;
import com.github.podd.restlet.PoddWebServiceApplication;
import com.github.podd.restlet.RestletUtils;
import com.github.podd.utils.DebugUtils;
import com.github.podd.utils.InferredOWLOntologyID;
import com.github.podd.utils.OntologyUtils;
import com.github.podd.utils.PODD;
import com.github.podd.utils.PoddRoles;
import com.github.podd.utils.PoddWebConstants;
/**
*
* Resource which allows uploading an artifact in the form of an RDF file.
*
* @author kutila
*
*/
public class UploadArtifactResourceImpl extends AbstractPoddResourceImpl
{
private static final String UPLOAD_PAGE_TITLE_TEXT = "PODD Upload New Artifact";
private final Path tempDirectory;
/**
* Constructor: prepare temp directory
*/
public UploadArtifactResourceImpl()
{
super();
try
{
this.tempDirectory = Files.createTempDirectory("podd-ontologymanageruploads");
}
catch(final IOException e)
{
this.log.error("Could not create temporary directory for ontology upload", e);
throw new RuntimeException("Could not create temporary directory", e);
}
}
private InferredOWLOntologyID doUpload(final Representation entity) throws ResourceException
{
final User user = this.getRequest().getClientInfo().getUser();
this.log.info("authenticated user: {}", user);
if(entity == null)
{
// POST request with no entity.
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Did not submit anything");
}
this.log.info("media-type: {}", entity.getMediaType());
InferredOWLOntologyID artifactMap;
if(MediaType.MULTIPART_FORM_DATA.equals(entity.getMediaType(), true))
{
// - extract file from incoming Representation and load artifact to
// PODD
artifactMap = this.uploadFileAndLoadArtifactIntoPodd(entity);
}
else
{
String formatString = this.getQuery().getFirstValue("format", true);
if(formatString == null)
{
// Use the media type that was attached to the entity as a
// fallback if they did not
// specify it as a query parameter
formatString = entity.getMediaType().getName();
}
final RDFFormat format = Rio.getParserFormatForMIMEType(formatString, RDFFormat.RDFXML);
// - optional parameter 'isforce'
DanglingObjectPolicy danglingObjectPolicy = DanglingObjectPolicy.REPORT;
final String forceStr = this.getQuery().getFirstValue(PoddWebConstants.KEY_EDIT_WITH_FORCE, true);
if(forceStr != null && Boolean.valueOf(forceStr))
{
danglingObjectPolicy = DanglingObjectPolicy.FORCE_CLEAN;
}
// - optional parameter 'verifyfilerefs'
DataReferenceVerificationPolicy fileRefVerificationPolicy = DataReferenceVerificationPolicy.DO_NOT_VERIFY;
final String fileRefVerifyStr =
this.getQuery().getFirstValue(PoddWebConstants.KEY_EDIT_VERIFY_FILE_REFERENCES, true);
if(fileRefVerifyStr != null && Boolean.valueOf(fileRefVerifyStr))
{
fileRefVerificationPolicy = DataReferenceVerificationPolicy.VERIFY;
}
try (final InputStream inputStream = entity.getStream();)
{
if(inputStream == null)
{
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Did not send an artifact");
}
artifactMap =
this.uploadFileAndLoadArtifactIntoPodd(inputStream, format, danglingObjectPolicy,
fileRefVerificationPolicy);
}
catch(final IOException e)
{
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "There was a problem with the input", e);
}
}
// Map uploading user as Project Administrator for this artifact so that
// they can edit it
// and assign permissions to it in the future
final PoddSesameRealm realm = this.getPoddApplication().getRealm();
realm.map(this.getRequest().getClientInfo().getUser(), PoddRoles.PROJECT_ADMIN.getRole(), artifactMap
.getOntologyIRI().toOpenRDFURI());
realm.map(this.getRequest().getClientInfo().getUser(), PoddRoles.PROJECT_PRINCIPAL_INVESTIGATOR.getRole(),
artifactMap.getOntologyIRI().toOpenRDFURI());
return artifactMap;
}
/**
* Handle http GET request to serve the new artifact upload page.
*/
@Get
public Representation getUploadArtifactPage(final Representation entity) throws ResourceException
{
// even though this only does a page READ, we're checking authorization
// for CREATE since the
// page is for creating a new artifact via a file upload
this.checkAuthentication(PoddAction.ARTIFACT_CREATE);
this.log.info("@Get UploadArtifactFile Page");
final Map<String, Object> dataModel = RestletUtils.getBaseDataModel(this.getRequest());
dataModel.put("contentTemplate", "artifact_upload.html.ftl");
dataModel.put("pageTitle", UploadArtifactResourceImpl.UPLOAD_PAGE_TITLE_TEXT);
// Output the base template, with contentTemplate from the dataModel
// defining the
// template to use for the content in the body of the page
return RestletUtils.getHtmlRepresentation(
this.getPoddApplication().getPropertyUtil()
.get(PoddWebConstants.PROPERTY_TEMPLATE_BASE, PoddWebConstants.DEFAULT_TEMPLATE_BASE),
dataModel, MediaType.TEXT_HTML, this.getPoddApplication().getTemplateConfiguration());
}
/**
* Handle http POST submitting a new artifact file
*/
@Post(":html")
public Representation uploadArtifactFileHtml(final Representation entity) throws ResourceException
{
this.checkAuthentication(PoddAction.ARTIFACT_CREATE);
this.log.info("@Post UploadArtifactFile Page");
final InferredOWLOntologyID artifactMap = this.doUpload(entity);
this.log.info("Successfully loaded artifact {}", artifactMap);
// TODO - create and write to a template informing success
final Map<String, Object> dataModel = RestletUtils.getBaseDataModel(this.getRequest());
dataModel.put("contentTemplate", "artifact_upload.html.ftl");
dataModel.put("pageTitle", UploadArtifactResourceImpl.UPLOAD_PAGE_TITLE_TEXT);
// This is now an InferredOWLOntologyID
dataModel.put("artifact", artifactMap);
// Output the base template, with contentTemplate from the dataModel
// defining the
// template to use for the content in the body of the page
return RestletUtils.getHtmlRepresentation(
this.getPoddApplication().getPropertyUtil()
.get(PoddWebConstants.PROPERTY_TEMPLATE_BASE, PoddWebConstants.DEFAULT_TEMPLATE_BASE),
dataModel, MediaType.TEXT_HTML, this.getPoddApplication().getTemplateConfiguration());
}
@Post(":rdf|rj|json|ttl")
public Representation uploadArtifactToRdf(final Representation entity, final Variant variant)
throws ResourceException
{
this.checkAuthentication(PoddAction.ARTIFACT_CREATE);
this.log.info("@Post uploadArtifactFile RDF ({})", variant.getMediaType().getName());
final InferredOWLOntologyID artifactId = this.doUpload(entity);
this.log.info("Successfully loaded artifact {}", artifactId);
RepositoryConnection managementConnection = null;
try
{
managementConnection = this.getPoddRepositoryManager().getManagementRepositoryConnection();
if(this.log.isDebugEnabled())
{
DebugUtils.printContents(managementConnection, this.getPoddRepositoryManager()
.getArtifactManagementGraph());
}
}
catch(final OpenRDFException e)
{
this.log.error("Error debugging artifact management graph", e);
}
finally
{
try
{
if(managementConnection != null)
{
managementConnection.close();
}
}
catch(final RepositoryException e)
{
this.log.error("Error closing repository connection", e);
}
}
final ByteArrayOutputStream output = new ByteArrayOutputStream(8096);
final RDFWriter writer =
Rio.createWriter(Rio.getWriterFormatForMIMEType(variant.getMediaType().getName(), RDFFormat.RDFXML),
output);
try
{
writer.startRDF();
final Model model =
OntologyUtils.ontologyIDsToModel(Arrays.asList(artifactId), new LinkedHashModel(), false);
final Set<Resource> ontologies = model.filter(null, RDF.TYPE, OWL.ONTOLOGY).subjects();
for(final Resource nextOntology : ontologies)
{
writer.handleStatement(PODD.VF.createStatement(nextOntology, RDF.TYPE, OWL.ONTOLOGY));
for(final Value nextVersion : model.filter(nextOntology, OWL.VERSIONIRI, null).objects())
{
if(nextVersion instanceof URI)
{
writer.handleStatement(PODD.VF.createStatement(nextOntology, OWL.VERSIONIRI, nextVersion));
}
else
{
this.log.error("Not including version IRI that was not a URI: {}", nextVersion);
}
}
}
RepositoryConnection permanentConnection = null;
try
{
// FIXME: This should be a method inside of
// PoddArtifactManagerImpl
final Set<? extends OWLOntologyID> schemaImports =
this.getPoddArtifactManager().getSchemaImports(artifactId);
permanentConnection = this.getPoddRepositoryManager().getPermanentRepositoryConnection(schemaImports);
final URI topObjectIRI =
this.getPoddArtifactManager().getSesameManager()
.getTopObjectIRI(artifactId, permanentConnection);
writer.handleStatement(PODD.VF.createStatement(artifactId.getOntologyIRI().toOpenRDFURI(),
PODD.PODD_BASE_HAS_TOP_OBJECT, topObjectIRI));
final Set<Statement> topObjectTypes =
Iterations.asSet(permanentConnection.getStatements(topObjectIRI, RDF.TYPE, null, true,
artifactId.getVersionIRI().toOpenRDFURI()));
for(final Statement nextTopObjectType : topObjectTypes)
{
writer.handleStatement(PODD.VF.createStatement(nextTopObjectType.getSubject(),
nextTopObjectType.getPredicate(), nextTopObjectType.getObject()));
}
}
catch(final OpenRDFException | UnmanagedArtifactIRIException | UnmanagedArtifactVersionException
| UnmanagedSchemaIRIException | SchemaManifestException | UnsupportedRDFormatException
| IOException | RepositoryNotFoundException e)
{
this.log.error("Failed to get top object URI", e);
}
finally
{
if(permanentConnection != null)
{
try
{
permanentConnection.close();
}
catch(final RepositoryException e)
{
this.log.error("Failed to close connection", e);
}
}
}
writer.endRDF();
}
catch(final RDFHandlerException e)
{
throw new ResourceException(Status.SERVER_ERROR_INTERNAL, "Could not create response");
}
this.log.info("Returning from upload artifact {}", artifactId);
return new ByteArrayRepresentation(output.toByteArray(), MediaType.valueOf(writer.getRDFFormat()
.getDefaultMIMEType()));
}
/**
* Handle http POST submitting a new artifact file Returns a text String containing the added
* artifact's Ontology IRI.
*
*/
@Post(":txt")
public Representation uploadArtifactToText(final Representation entity, final Variant variant)
throws ResourceException
{
this.checkAuthentication(PoddAction.ARTIFACT_CREATE);
this.log.info("@Post uploadArtifactFile ({})", variant.getMediaType().getName());
final InferredOWLOntologyID artifactMap = this.doUpload(entity);
this.log.info("Successfully loaded artifact {}", artifactMap.getOntologyIRI().toString());
return new StringRepresentation(artifactMap.getOntologyIRI().toString());
}
/**
*
* @param inputStream
* The input stream containing an RDF document in the given format that is to be
* uploaded.
* @param format
* The determined, or at least specified, format for the serialised RDF triples in
* the input.
* @return
* @throws ResourceException
*/
private InferredOWLOntologyID uploadFileAndLoadArtifactIntoPodd(final InputStream inputStream,
final RDFFormat format, final DanglingObjectPolicy danglingObjectPolicy,
final DataReferenceVerificationPolicy dataReferenceVerificationPolicy) throws ResourceException
{
final PoddArtifactManager artifactManager =
((PoddWebServiceApplication)this.getApplication()).getPoddArtifactManager();
try
{
if(artifactManager != null)
{
final InferredOWLOntologyID loadedArtifact =
artifactManager.loadArtifact(inputStream, format, danglingObjectPolicy,
dataReferenceVerificationPolicy);
return loadedArtifact;
}
else
{
throw new ResourceException(Status.SERVER_ERROR_SERVICE_UNAVAILABLE,
"Could not find PODD Artifact Manager");
}
}
catch(final DuplicateArtifactIRIException e)
{
this.log.warn("Attempting to load duplicate artifact {}", e);
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Failed loading duplicate artifact to PODD", e);
}
catch(OpenRDFException | PoddException | IOException | OWLException e)
{
this.log.error("Failed to load artifact: {}", e.getMessage());
throw new ResourceException(Status.SERVER_ERROR_INTERNAL, "Error loading artifact to PODD", e);
}
}
private InferredOWLOntologyID uploadFileAndLoadArtifactIntoPodd(final Representation entity)
throws ResourceException
{
List<FileItem> items;
Path filePath = null;
String contentType = null;
// 1: Create a factory for disk-based file items
final DiskFileItemFactory factory = new DiskFileItemFactory(1000240, this.tempDirectory.toFile());
// 2: Create a new file upload handler
final RestletFileUpload upload = new RestletFileUpload(factory);
final Map<String, String> props = new HashMap<String, String>();
try
{
// 3: Request is parsed by the handler which generates a list of
// FileItems
items = upload.parseRequest(this.getRequest());
for(final FileItem fi : items)
{
final String name = fi.getName();
if(name == null)
{
props.put(fi.getFieldName(), new String(fi.get(), StandardCharsets.UTF_8));
}
else
{
// FIXME: Strip everything up to the last . out of the
// filename so that
// the filename can be used for content type determination
// where
// possible.
// InputStream uploadedFileInputStream =
// fi.getInputStream();
try
{
// Note: These are Java-7 APIs
contentType = fi.getContentType();
props.put("Content-Type", fi.getContentType());
filePath = Files.createTempFile(this.tempDirectory, "ontologyupload-", name);
final File file = filePath.toFile();
file.deleteOnExit();
fi.write(file);
}
catch(final IOException ioe)
{
throw ioe;
}
catch(final Exception e)
{
// avoid throwing a generic exception just because the
// apache
// commons library throws Exception
throw new IOException(e);
}
}
}
}
catch(final IOException | FileUploadException e)
{
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, e);
}
this.log.info("props={}", props.toString());
if(filePath == null)
{
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Did not submit a valid file and filename");
}
this.log.info("filename={}", filePath.toAbsolutePath().toString());
this.log.info("contentType={}", contentType);
RDFFormat format = null;
// If the content type was application/octet-stream then use the file
// name instead
// Browsers attach this content type when they are not sure what the
// real type is
if(MediaType.APPLICATION_OCTET_STREAM.getName().equals(contentType))
{
format = Rio.getParserFormatForFileName(filePath.getFileName().toString());
this.log.info("octet-stream contentType filename format={}", format);
}
// Otherwise use the content type directly in preference to using the
// filename
else if(contentType != null)
{
format = Rio.getParserFormatForMIMEType(contentType);
this.log.info("non-octet-stream contentType format={}", format);
}
// If the content type choices failed to resolve the type, then try the
// filename
if(format == null)
{
format = Rio.getParserFormatForFileName(filePath.getFileName().toString());
this.log.info("non-content-type filename format={}", format);
}
// Or fallback to RDF/XML which at minimum is able to detect when the
// document is
// structurally invalid
if(format == null)
{
this.log.warn("Could not determine RDF format from request so falling back to RDF/XML");
format = RDFFormat.RDFXML;
}
try (final InputStream inputStream =
new BufferedInputStream(Files.newInputStream(filePath, StandardOpenOption.READ));)
{
return this.uploadFileAndLoadArtifactIntoPodd(inputStream, format, DanglingObjectPolicy.REPORT,
DataReferenceVerificationPolicy.DO_NOT_VERIFY);
}
catch(final IOException e)
{
throw new ResourceException(Status.SERVER_ERROR_INTERNAL, "File IO error occurred", e);
}
}
}