com.github.podd.resources.UploadArtifactResourceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.github.podd.resources.UploadArtifactResourceImpl.java

Source

/**
 * 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);
        }

    }

}