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