org.opendatakit.api.odktables.FileService.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.api.odktables.FileService.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.api.odktables;

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

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
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.lang3.ArrayUtils;
import org.opendatakit.ContextUtils;
import org.opendatakit.aggregate.odktables.rest.ApiConstants;
import org.opendatakit.aggregate.odktables.rest.entity.TableRole.TablePermission;
import org.opendatakit.constants.BasicConsts;
import org.opendatakit.constants.WebConsts;
import org.opendatakit.context.CallingContext;
import org.opendatakit.odktables.ConfigFileChangeDetail;
import org.opendatakit.odktables.FileContentInfo;
import org.opendatakit.odktables.FileManager;
import org.opendatakit.odktables.exception.FileNotFoundException;
import org.opendatakit.odktables.exception.PermissionDeniedException;
import org.opendatakit.odktables.relation.DbTableFileInfo;
import org.opendatakit.odktables.security.TablesUserPermissions;
import org.opendatakit.persistence.exception.ODKDatastoreException;
import org.opendatakit.persistence.exception.ODKEntityNotFoundException;
import org.opendatakit.persistence.exception.ODKOverQuotaException;
import org.opendatakit.persistence.exception.ODKTaskLockException;
import org.opendatakit.security.common.GrantedAuthorityName;
import org.opendatakit.security.server.SecurityServiceUtil;
import org.springframework.http.MediaType;

import io.swagger.annotations.Api;
import io.swagger.annotations.Authorization;

/**
 * Servlet for handling the uploading and downloading of files from the phone.
 * <p>
 * The general idea is that the interaction with the actual files will occur at
 * /odktables/files/unrootedPathToFile. Files will thus be referred to by their unrooted path
 * relative to the /odk/tables/ directory on the device.
 * <p>
 * A GET request to that url will download the file. A POST request to that url must contain an
 * entity that is the file, as well as a table id parameter on the POST itself.
 * <p>
 * These urls should be generated by a file manifest servlet on a table id basis.
 *
 * @author sudar.sam@gmail.com
 *
 */
@Api(authorizations = { @Authorization(value = "basicAuth") })
public class FileService {

    public static final String PARAM_AS_ATTACHMENT = "as_attachment";
    public static final String ERROR_MSG_INSUFFICIENT_PATH = "Not Enough Path Segments: must be at least 2.";
    public static final String ERROR_MSG_UNRECOGNIZED_APP_ID = "Unrecognized app id: ";
    public static final String ERROR_MSG_PATH_NOT_UNDER_APP_ID = "File path is not under app id: ";
    public static final String MIME_TYPE_IMAGE_JPEG = "image/jpeg";

    private final HttpServletRequest req;
    private final CallingContext callingContext;
    private final String appId;
    private final UriInfo info;
    private TablesUserPermissions userPermissions;

    public FileService(HttpServletRequest req, UriInfo info, String appId, CallingContext cc)
            throws ODKEntityNotFoundException, ODKDatastoreException, PermissionDeniedException,
            ODKTaskLockException {
        this.req = req;
        this.callingContext = cc;
        this.appId = appId;
        this.info = info;
        this.userPermissions = ContextUtils.getTablesUserPermissions(cc);

    }

    @GET
    @Path("{odkClientVersion}/{filePath:.*}")
    public Response getFile(@Context HttpHeaders httpHeaders,
            @PathParam("odkClientVersion") String odkClientVersion,
            @PathParam("filePath") List<PathSegment> segments, @QueryParam(PARAM_AS_ATTACHMENT) String asAttachment)
            throws IOException, ODKTaskLockException, ODKEntityNotFoundException, ODKOverQuotaException,
            FileNotFoundException, PermissionDeniedException, ODKDatastoreException {

        // First we need to get the table id from the path. We're
        // going to be assuming that you're passing the entire path of the file
        // under /sdcard/opendatakit/appId/ e.g., tables/tableid/the/rest/of/path.
        // So we'll reclaim the tidbits and then reconstruct the entire path.
        // If you are getting general files, there will be no recoverable tableId,
        // and these are then app-level files.
        if (segments.size() < 1) {
            return Response.status(Status.BAD_REQUEST).entity(FileService.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();
        }
        String appRelativePath = constructPathFromSegments(segments);
        String tableId = FileManager.getTableIdForFilePath(appRelativePath);

        FileContentInfo fileContentInfo = null;

        // DbTableFileInfo.NO_TABLE_ID -- means that we are working with app-level
        // permissions
        if (!DbTableFileInfo.NO_TABLE_ID.equals(tableId)) {
            userPermissions.checkPermission(appId, tableId, TablePermission.READ_PROPERTIES);
        }

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

        FileManager fm = new FileManager(appId, callingContext);
        fileContentInfo = fm.getFile(odkClientVersion, tableId, appRelativePath);

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

            // test if we should return a NOT_MODIFIED response...
            if (eTag != null && eTag.equals(fileContentInfo.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();
            }

            javax.ws.rs.core.MediaType mediaType = javax.ws.rs.core.MediaType.valueOf(fileContentInfo.contentType);
            if (mediaType.equals(javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE)
                    || fileContentInfo.contentType.startsWith("text")) {
                mediaType = mediaType.withCharset("utf-8");
            }

            ResponseBuilder rBuild = Response.ok(fileContentInfo.fileBlob).type(mediaType)
                    .header(HttpHeaders.CONTENT_LENGTH, fileContentInfo.contentLength)
                    .header(HttpHeaders.ETAG, fileContentInfo.contentHash)
                    .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(WebConsts.CONTENT_DISPOSITION,
                        "attachment; " + "filename=\"" + appRelativePath + "\"");
            }
            return rBuild.build();
        } else {
            return Response.status(Status.NOT_FOUND)
                    .entity("File content not yet available for: " + appRelativePath)
                    .header(ApiConstants.OPEN_DATA_KIT_VERSION_HEADER, ApiConstants.OPEN_DATA_KIT_VERSION)
                    .header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Credentials", "true")
                    .build();
        }
    }

    @POST
    @Path("{odkClientVersion}/{filePath:.*}")
    public Response putFile(@PathParam("odkClientVersion") String odkClientVersion,
            @PathParam("filePath") List<PathSegment> segments, byte[] content)
            throws IOException, ODKTaskLockException, PermissionDeniedException, ODKDatastoreException {

        TreeSet<GrantedAuthorityName> ui = SecurityServiceUtil.getCurrentUserSecurityInfo(callingContext);
        if (!ui.contains(GrantedAuthorityName.ROLE_ADMINISTER_TABLES)) {
            throw new PermissionDeniedException("User does not belong to the 'Administer Tables' group");
        }

        if (segments.size() < 1) {
            return Response.status(Status.BAD_REQUEST).entity(FileService.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();
        }
        String appRelativePath = constructPathFromSegments(segments);
        String tableId = FileManager.getTableIdForFilePath(appRelativePath);
        String contentType = req.getContentType();

        // DbTableFileInfo.NO_TABLE_ID -- means that we are working with app-level
        // permissions
        if (!DbTableFileInfo.NO_TABLE_ID.equals(tableId)) {
            userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_PROPERTIES);
        }

        FileManager fm = new FileManager(appId, callingContext);

        FileContentInfo fi = new FileContentInfo(appRelativePath, contentType, Long.valueOf(content.length), null,
                content);

        ConfigFileChangeDetail outcome = fm.putFile(odkClientVersion, tableId, fi, userPermissions);

        UriBuilder ub = info.getBaseUriBuilder();
        ub.path(OdkTables.class, "getFilesService");
        URI self = ub.path(FileService.class, "getFile")
                .build(ArrayUtils.toArray(appId, odkClientVersion, appRelativePath), false);

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

        return Response.status((outcome == ConfigFileChangeDetail.FILE_UPDATED) ? Status.ACCEPTED : 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();
    }

    /**
     * Delete only works on full file paths -- you cannot specify a partial path or wildcard (*) path.
     *
     * @param odkClientVersion
     * @param segments
     * @return
     * @throws IOException
     * @throws ODKTaskLockException
     * @throws ODKOverQuotaException
     * @throws ODKEntityNotFoundException
     * @throws ODKDatastoreException
     * @throws PermissionDeniedException
     */
    @DELETE
    @Path("{odkClientVersion}/{filePath:.*}")
    public Response deleteFile(@PathParam("odkClientVersion") String odkClientVersion,
            @PathParam("filePath") List<PathSegment> segments) throws IOException, ODKTaskLockException,
            ODKEntityNotFoundException, ODKOverQuotaException, PermissionDeniedException, ODKDatastoreException {

        TreeSet<GrantedAuthorityName> ui = SecurityServiceUtil.getCurrentUserSecurityInfo(callingContext);
        if (!ui.contains(GrantedAuthorityName.ROLE_ADMINISTER_TABLES)) {
            throw new PermissionDeniedException("User does not belong to the 'Administer Tables' group");
        }

        if (segments.size() < 1) {
            return Response.status(Status.BAD_REQUEST).entity(FileService.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();
        }
        String appRelativePath = constructPathFromSegments(segments);
        String tableId = FileManager.getTableIdForFilePath(appRelativePath);

        // DbTableFileInfo.NO_TABLE_ID -- means that we are working with app-level
        // permissions
        if (!DbTableFileInfo.NO_TABLE_ID.equals(tableId)) {
            userPermissions.checkPermission(appId, tableId, TablePermission.WRITE_PROPERTIES);
        }

        FileManager fm = new FileManager(appId, callingContext);
        fm.deleteFile(odkClientVersion, tableId, appRelativePath);

        return Response.ok().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 /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;
    }

}