org.opendatakit.aggregate.odktables.impl.api.InstanceFileServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.aggregate.odktables.impl.api.InstanceFileServiceImpl.java

Source

/*
 * Copyright (C) 2012-2013 University of Washington
 *
 * 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 org.opendatakit.aggregate.odktables.impl.api;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opendatakit.aggregate.constants.ErrorConsts;
import org.opendatakit.aggregate.odktables.api.InstanceFileService;
import org.opendatakit.aggregate.odktables.api.OdkTables;
import org.opendatakit.aggregate.odktables.api.RealizedTableService;
import org.opendatakit.aggregate.odktables.api.TableService;
import org.opendatakit.aggregate.odktables.exception.PermissionDeniedException;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceFiles;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceManifestETags;
import org.opendatakit.aggregate.odktables.relation.DbTableInstanceManifestETags.DbTableInstanceManifestETagEntity;
import org.opendatakit.aggregate.odktables.rest.ApiConstants;
import org.opendatakit.aggregate.odktables.rest.entity.Error;
import org.opendatakit.aggregate.odktables.rest.entity.OdkTablesFileManifest;
import org.opendatakit.aggregate.odktables.rest.entity.OdkTablesFileManifestEntry;
import org.opendatakit.aggregate.odktables.rest.entity.Error.ErrorType;
import org.opendatakit.aggregate.odktables.rest.entity.TableRole.TablePermission;
import org.opendatakit.aggregate.odktables.security.TablesUserPermissions;
import org.opendatakit.common.datamodel.BinaryContentManipulator.BlobSubmissionOutcome;
import org.opendatakit.common.ermodel.BlobEntitySet;
import org.opendatakit.common.persistence.PersistenceUtils;
import org.opendatakit.common.persistence.exception.ODKDatastoreException;
import org.opendatakit.common.persistence.exception.ODKEntityNotFoundException;
import org.opendatakit.common.persistence.exception.ODKTaskLockException;
import org.opendatakit.common.web.CallingContext;
import org.opendatakit.common.web.constants.BasicConsts;
import org.opendatakit.common.web.constants.HtmlConsts;

public class InstanceFileServiceImpl implements InstanceFileService {

    private static final Log LOGGER = LogFactory.getLog(InstanceFileServiceImpl.class);

    /**
     * String to stand in for those things in the app's root directory.
     *
     * NOTE: This cannot be null -- GAE doesn't like that!
     */
    public static final String NO_TABLE_ID = "";

    private static final String ERROR_FILE_VERSION_DIFFERS = "File on server does not match file being uploaded. Aborting upload. ";

    /**
     * The name of the folder that contains the files associated with a table in
     * an app.
     *
     * @see #getTableIdFromPathSegments(List)
     */
    private final CallingContext cc;
    private final TablesUserPermissions userPermissions;
    private final UriInfo info;
    private final String appId;
    private final String tableId;
    private final String rowId;
    private final String schemaETag;

    public InstanceFileServiceImpl(String appId, String tableId, String schemaETag, String rowId, UriInfo info,
            TablesUserPermissions userPermissions, CallingContext cc)
            throws ODKEntityNotFoundException, ODKDatastoreException {
        this.cc = cc;
        this.appId = appId;
        this.tableId = tableId;
        this.rowId = rowId;
        this.schemaETag = schemaETag;
        this.info = info;
        this.userPermissions = userPermissions;
    }

    @Override
    public Response getManifest(@Context HttpHeaders httpHeaders,
            @QueryParam(PARAM_AS_ATTACHMENT) String asAttachment) throws IOException {

        UriBuilder ub = info.getBaseUriBuilder();
        ub.path(OdkTables.class, "getTablesService");
        UriBuilder full = ub.clone().path(TableService.class, "getRealizedTable")
                .path(RealizedTableService.class, "getInstanceFiles")
                .path(InstanceFileService.class, "getManifest");
        URI self = full.build(appId, tableId, schemaETag, rowId);
        String manifestUrl = self.toURL().toExternalForm();

        // retrieve the incoming if-none-match eTag...
        List<String> eTags = httpHeaders.getRequestHeader(HttpHeaders.IF_NONE_MATCH);
        String eTag = (eTags == null || eTags.isEmpty()) ? null : eTags.get(0);
        DbTableInstanceManifestETagEntity eTagEntity = null;
        try {
            try {
                eTagEntity = DbTableInstanceManifestETags.getRowIdEntry(tableId, rowId, cc);
            } catch (ODKEntityNotFoundException e) {
                // ignore...
            }

            if (eTag != null && eTagEntity != null && eTag.equals(eTagEntity.getManifestETag())) {
                return Response.status(Status.NOT_MODIFIED).header(HttpHeaders.ETAG, eTag)
                        .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                        .header("Access-Control-Allow-Origin", "*")
                        .header("Access-Control-Allow-Credentials", "true").build();
            }

            userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW);

            ArrayList<OdkTablesFileManifestEntry> manifestEntries = new ArrayList<OdkTablesFileManifestEntry>();
            DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
            BlobEntitySet instance = blobStore.getBlobEntitySet(rowId, cc);

            int count = instance.getAttachmentCount(cc);
            for (int i = 1; i <= count; ++i) {
                OdkTablesFileManifestEntry entry = new OdkTablesFileManifestEntry();
                entry.filename = instance.getUnrootedFilename(i, cc);
                entry.contentLength = instance.getContentLength(i, cc);
                entry.contentType = instance.getContentType(i, cc);
                entry.md5hash = instance.getContentHash(i, cc);

                URI getFile = ub.clone().path(TableService.class, "getRealizedTable")
                        .path(RealizedTableService.class, "getInstanceFiles")
                        .path(InstanceFileService.class, "getFile")
                        .build(appId, tableId, schemaETag, rowId, entry.filename);
                String locationUrl = getFile.toURL().toExternalForm();
                entry.downloadUrl = locationUrl;

                manifestEntries.add(entry);
            }
            OdkTablesFileManifest manifest = new OdkTablesFileManifest(manifestEntries);

            String newETag = Integer.toHexString(manifest.hashCode());
            // create a new eTagEntity if there isn't one already...
            if (eTagEntity == null) {
                eTagEntity = DbTableInstanceManifestETags.createNewEntity(tableId, rowId, cc);
                eTagEntity.setManifestETag(newETag);
                eTagEntity.put(cc);
            } else if (!newETag.equals(eTagEntity.getManifestETag())) {
                Log log = LogFactory.getLog(FileManifestServiceImpl.class);
                log.error("TableInstance (" + tableId + "," + rowId
                        + ") Manifest ETag does not match computed value!");
                eTagEntity.setManifestETag(newETag);
                eTagEntity.put(cc);
            }

            // and whatever the eTag is in that entity is the eTag we should return...
            eTag = eTagEntity.getManifestETag();

            ResponseBuilder rBuild = Response.ok(manifest).header(HttpHeaders.ETAG, eTag)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true");
            if (asAttachment != null && !"".equals(asAttachment)) {
                // Set the filename we're downloading to the disk.
                rBuild.header(HtmlConsts.CONTENT_DISPOSITION,
                        "attachment; " + "filename=\"" + "manifest.json" + "\"");
            }
            return rBuild.build();
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            return Response.status(Status.INTERNAL_SERVER_ERROR)
                    .entity("Unable to retrieve manifest of attachments for: " + manifestUrl)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        } catch (PermissionDeniedException e) {
            String msg = e.getMessage();
            if (msg == null) {
                msg = e.toString();
            }
            LOGGER.error(("ODKTables file upload permissions error: " + msg));
            return Response.status(Status.FORBIDDEN).entity(new Error(ErrorType.PERMISSION_DENIED, msg))
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
    }

    @Override
    public Response getFile(@Context HttpHeaders httpHeaders, @PathParam("filePath") List<PathSegment> segments,
            @QueryParam(PARAM_AS_ATTACHMENT) String asAttachment) throws IOException {
        // The appId and tableId are from the surrounding TableService.
        // The rowId is already pulled out.
        // The segments are just rest/of/path in the full app-centric
        // path of:
        // appid/data/attachments/tableid/instances/instanceId/rest/of/path
        if (rowId == null || rowId.length() == 0) {
            return Response.status(Status.BAD_REQUEST).entity(InstanceFileService.ERROR_MSG_INVALID_ROW_ID)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
        if (segments.size() < 1) {
            return Response.status(Status.BAD_REQUEST).entity(InstanceFileService.ERROR_MSG_INSUFFICIENT_PATH)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
        // Now construct the whole path.
        String partialPath = constructPathFromSegments(segments);

        // retrieve the incoming if-none-match eTag...
        List<String> eTags = httpHeaders.getRequestHeader(HttpHeaders.IF_NONE_MATCH);
        String eTag = (eTags == null || eTags.isEmpty()) ? null : eTags.get(0);

        UriBuilder ub = info.getBaseUriBuilder();
        ub.path(OdkTables.class, "getTablesService");

        URI getFile = ub.clone().path(TableService.class, "getRealizedTable")
                .path(RealizedTableService.class, "getInstanceFiles").path(InstanceFileService.class, "getFile")
                .build(appId, tableId, schemaETag, rowId, partialPath);

        String locationUrl = getFile.toURL().toExternalForm();

        try {
            userPermissions.checkPermission(appId, tableId, TablePermission.READ_ROW);

            DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
            BlobEntitySet instance = blobStore.getBlobEntitySet(rowId, cc);

            int count = instance.getAttachmentCount(cc);
            for (int i = 1; i <= count; ++i) {
                String path = instance.getUnrootedFilename(i, cc);
                if (path != null && path.equals(partialPath)) {
                    byte[] fileBlob = instance.getBlob(i, cc);
                    String contentType = instance.getContentType(i, cc);
                    String contentHash = instance.getContentHash(i, cc);
                    Long contentLength = instance.getContentLength(i, cc);

                    // And now prepare everything to be returned to the caller.
                    if (fileBlob != null && contentType != null && contentLength != null && contentLength != 0L) {

                        // test if we should return a NOT_MODIFIED response...
                        if (eTag != null && eTag.equals(contentHash)) {
                            return Response.status(Status.NOT_MODIFIED).header(HttpHeaders.ETAG, eTag)
                                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER,
                                            ApiConstants.OPEN_DATA_KIT_VERSION)
                                    .header("Access-Control-Allow-Origin", "*")
                                    .header("Access-Control-Allow-Credentials", "true").build();
                        }

                        ResponseBuilder rBuild = Response.ok(fileBlob, contentType)
                                .header(HttpHeaders.ETAG, contentHash)
                                .header(HttpHeaders.CONTENT_LENGTH, contentLength)
                                .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER,
                                        ApiConstants.OPEN_DATA_KIT_VERSION)
                                .header("Access-Control-Allow-Origin", "*")
                                .header("Access-Control-Allow-Credentials", "true");
                        if (asAttachment != null && !"".equals(asAttachment)) {
                            // Set the filename we're downloading to the disk.
                            rBuild.header(HtmlConsts.CONTENT_DISPOSITION,
                                    "attachment; " + "filename=\"" + partialPath + "\"");
                        }
                        return rBuild.build();
                    } else {
                        return Response.status(Status.NOT_FOUND)
                                .entity("File content not yet available for: " + locationUrl)
                                .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER,
                                        ApiConstants.OPEN_DATA_KIT_VERSION)
                                .header("Access-Control-Allow-Origin", "*")
                                .header("Access-Control-Allow-Credentials", "true").build();
                    }

                }
            }
            return Response.status(Status.NOT_FOUND).entity("No file found for: " + locationUrl)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        } catch (ODKDatastoreException e) {
            e.printStackTrace();
            return Response.status(Status.INTERNAL_SERVER_ERROR)
                    .entity("Unable to retrieve attachment and access attributes for: " + locationUrl)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        } catch (PermissionDeniedException e) {
            String msg = e.getMessage();
            if (msg == null) {
                msg = e.toString();
            }
            LOGGER.error(("ODKTables file upload permissions error: " + msg));
            return Response.status(Status.FORBIDDEN).entity(new Error(ErrorType.PERMISSION_DENIED, msg))
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
    }

    @Override
    public Response putFile(@Context HttpServletRequest req, @PathParam("filePath") List<PathSegment> segments,
            byte[] content) throws IOException, ODKTaskLockException {

        if (segments.size() < 1) {
            return Response.status(Status.BAD_REQUEST).entity(InstanceFileService.ERROR_MSG_INSUFFICIENT_PATH)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
        // The appId and tableId are from the surrounding TableService.
        // The rowId is already pulled out.
        // The segments are just rest/of/path in the full app-centric
        // path of:
        // appid/data/attachments/tableid/instances/instanceId/rest/of/path
        String partialPath = constructPathFromSegments(segments);
        String contentType = req.getContentType();
        String md5Hash = PersistenceUtils.newMD5HashUri(content);
        try {
            userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_ROW);

            UriBuilder ub = info.getBaseUriBuilder();
            ub.path(OdkTables.class, "getTablesService");

            URI getFile = ub.clone().path(TableService.class, "getRealizedTable")
                    .path(RealizedTableService.class, "getInstanceFiles").path(InstanceFileService.class, "getFile")
                    .build(appId, tableId, schemaETag, rowId, partialPath);

            String locationUrl = getFile.toURL().toExternalForm();

            // we are adding a file -- delete any cached ETag value for this row's attachments manifest
            try {
                DbTableInstanceManifestETagEntity entity = DbTableInstanceManifestETags.getRowIdEntry(tableId,
                        rowId, cc);
                entity.delete(cc);
            } catch (ODKEntityNotFoundException e) {
                // ignore...
            }

            DbTableInstanceFiles blobStore = new DbTableInstanceFiles(tableId, cc);
            BlobEntitySet instance = blobStore.newBlobEntitySet(rowId, cc);
            int count = instance.getAttachmentCount(cc);
            for (int i = 1; i <= count; ++i) {
                String path = instance.getUnrootedFilename(i, cc);
                if (path != null && path.equals(partialPath)) {
                    // we already have this in our store -- check that it is identical.
                    // if not, we have a problem!!!
                    if (md5Hash.equals(instance.getContentHash(i, cc))) {
                        return Response.status(Status.CREATED).header("Location", locationUrl)
                                .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER,
                                        ApiConstants.OPEN_DATA_KIT_VERSION)
                                .header("Access-Control-Allow-Origin", "*")
                                .header("Access-Control-Allow-Credentials", "true").build();
                    } else {
                        return Response.status(Status.BAD_REQUEST)
                                .entity(ERROR_FILE_VERSION_DIFFERS + "\n" + partialPath)
                                .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER,
                                        ApiConstants.OPEN_DATA_KIT_VERSION)
                                .header("Access-Control-Allow-Origin", "*")
                                .header("Access-Control-Allow-Credentials", "true").build();
                    }
                }
            }
            BlobSubmissionOutcome outcome = instance.addBlob(content, contentType, partialPath, false, cc);
            if (outcome == BlobSubmissionOutcome.NEW_FILE_VERSION) {
                return Response.status(Status.BAD_REQUEST).entity(ERROR_FILE_VERSION_DIFFERS + "\n" + partialPath)
                        .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                        .header("Access-Control-Allow-Origin", "*")
                        .header("Access-Control-Allow-Credentials", "true").build();
            }
            return Response.status(Status.CREATED).header("Location", locationUrl)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        } catch (ODKDatastoreException e) {
            LOGGER.error(("ODKTables file upload persistence error: " + e.getMessage()));
            return Response.status(Status.INTERNAL_SERVER_ERROR)
                    .entity(ErrorConsts.PERSISTENCE_LAYER_PROBLEM + "\n" + e.getMessage())
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        } catch (PermissionDeniedException e) {
            String msg = e.getMessage();
            if (msg == null) {
                msg = e.toString();
            }
            LOGGER.error(("ODKTables file upload permissions error: " + msg));
            return Response.status(Status.FORBIDDEN).entity(new Error(ErrorType.PERMISSION_DENIED, msg))
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
    }

    /**
     * Construct the path for the file. This is the entire path excluding the app
     * id.
     *
     * @param segments
     * @return
     */
    private String constructPathFromSegments(List<PathSegment> segments) {
        // Now construct up the path from the segments.
        // We are NOT going to include the app id. Therefore if you upload a file
        // with a path of appid/myDir/myFile.html, the path will be stored as
        // myDir/myFile.html. This is so that when you get the filename on the
        // manifest, it won't matter what is the root directory of your app on your
        // device. Otherwise you might have to strip the first path segment or do
        // something similar.
        StringBuilder sb = new StringBuilder();
        int i = 0;
        for (PathSegment segment : segments) {
            sb.append(segment.getPath());
            if (i < segments.size() - 1) {
                sb.append(BasicConsts.FORWARDSLASH);
            }
            i++;
        }
        String wholePath = sb.toString();
        return wholePath;
    }

}