org.geoserver.restupload.ResumableUploadCatalogResource.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.restupload.ResumableUploadCatalogResource.java

Source

/* (c) 2015 Open Source Geospatial Foundation - all rights reserved
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */

package org.geoserver.restupload;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.StringUtils;

import org.geoserver.catalog.Catalog;
import org.geoserver.rest.util.RESTUtils;
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.restlet.Context;
import org.restlet.data.Form;
import org.restlet.data.MediaType;

import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import org.restlet.resource.StringRepresentation;
import org.restlet.util.Series;

/**
 * The main feature of the following module is the ability to resume the upload process of a file via REST.
 * <p>
 * An upload URL is generated at the first REST POST request and saved in order to execute the other upload steps.</br>
 * Successive PUT request on the URL created before allow partial or full upload of binary file.</br>
 * RANGE parameter in PUT request/response allow handshake of the number of bytes currently uploaded.</br>
 * GET request can be used to retrieve informations about upload status.
 * <p>
 * The uploaded resource is stored to temporary folder until the upload is not completed or the
 *  {@link ResumableUploadResourceCleaner#expirationDelay} time is elapsed.</br>
 *  When the upload is terminated the file is moved to REST main folder by
 *  {@link ResumableUploadPathMapper}
 *
 *
 * @author Nicola Lagomarsini
 *
 */

public class ResumableUploadCatalogResource extends Resource {

    private static final Logger LOGGER = Logging.getLogger(ResumableUploadCatalogResource.class);

    /** Manager for the Resumable REST upload */
    private ResumableUploadResourceManager resumableUploadResourceManager;

    /**
     * If the server has successfully received all bytes from the operation, it responds with a final status code;
     * otherwise it responds with a 308
     * (Resume Incomplete), indicating which bytes of the operation it has successfully received.
     */
    public static final Status RESUME_INCOMPLETE = new Status(308);

    public ResumableUploadCatalogResource(Context context, Request request, Response response, Catalog catalog,
            ResumableUploadResourceManager resumableUploadResourceManager) {
        super(context, request, response);
        this.resumableUploadResourceManager = resumableUploadResourceManager;
    }

    @Override
    public boolean allowPost() {
        return true;
    }

    /**
     * PUT request is allow only if at least one upload is in progress
     */
    @Override
    public boolean allowPut() {
        return resumableUploadResourceManager.hasAnyResource();
    }

    @Override
    public boolean allowGet() {
        return true;
    }

    /**
     * POST request returns upload URL with uploadId to call with successive PUT request.</br>
     * The body of POST request must contains the desired final file path,
     * it can be relative path with subfolder.
     */
    @Override
    public void handlePost() {
        try {
            String filePath = getRequest().getEntity().getText();
            if (filePath == null || filePath.isEmpty()) {
                getResponse().setStatus(
                        new Status(Status.CLIENT_ERROR_BAD_REQUEST, "POST data must contains upload file path"));
                return;
            }
            Reference ref = getRequest().getResourceRef();
            String baseURL = ref.getIdentifier();

            String uploadId = resumableUploadResourceManager.createUploadResource(filePath);

            Representation output = new StringRepresentation(
                    "-----TO USE IN PUT-----\n" + baseURL + "/" + uploadId + "\n-----------------------\n",
                    MediaType.TEXT_PLAIN);
            Response response = getResponse();

            Series<Parameter> headers = new Form();
            headers.add("Location", baseURL + uploadId);
            getResponse().getAttributes().put("org.restlet.http.headers", headers);
            response.setEntity(output);
            response.setStatus(Status.SUCCESS_CREATED);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            getResponse().setStatus(new Status(Status.SERVER_ERROR_INTERNAL, e.getMessage()));
            return;
        }
    }

    /**
     * PUT request is used to uploads file. </br>
     * The request must contains the uploadId attribute with the value returned by previous POST request. </br>
     * If the PUT request is the first, it must contains the header parameters "Content-Length: {total file size in bytes}"
     * Successive resume PUT request must contains the header parameters:</br>
     * <ul>
     *  <li>Content-Length:{total size of bytes which must be uploaded}
     *  <li>Content-Range:{resume byte start byte index}-{file end byte index}/{total file size in bytes}
     * </ul>
     * If the upload is incomplete, the PUT return the RANGE header attribute:</br>
     * Range: 0-{uploded end byte index}.
     * If the upload is complete, the uploaded file is moved to REST root folder and the PUT return
     * the relative path of the file.</br>
     * Sidecar file is created in temporary folder to mark the upload as ended and provide information to
     * successive GET requests.
     */
    @Override
    public void handlePut() {
        /*
         * Check required parameters: - uploadId - Content-Length
         */
        String uploadId = RESTUtils.getAttribute(getRequest(), "uploadId");
        if (uploadId == null || uploadId.isEmpty()) {
            getResponse().setStatus(new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Missing upload ID"));
            return;
        }
        if (!resumableUploadResourceManager.resourceExists(uploadId)) {
            getResponse().setStatus(new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Unknow upload ID"));
            return;
        }
        Long totalByteToUpload = getContentLength();
        Long startPosition = 0L;
        Long endPosition = (totalByteToUpload - 1);
        Long totalFileSize = totalByteToUpload;
        if (totalByteToUpload == 0) {
            getResponse().setStatus(new Status(Status.CLIENT_ERROR_LENGTH_REQUIRED,
                    "Not zero Content-Length header must be specified"));
            return;
        }
        HeaderRange headerRange = getHeaderRange();
        if (headerRange != null) {
            try {
                if (headerRange.getMinimum() > headerRange.getMaximum()
                        || (headerRange.getRange().longValue() != totalByteToUpload)) {
                    getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST,
                            "Range parameter values are not valid");
                    return;
                }
                startPosition = headerRange.getMinimum().longValue();
                endPosition = headerRange.getMaximum().longValue();
                totalFileSize = headerRange.getTotalFileSize();
                /*
                 * Validate resume request If resume is requested existing file must contains the
                 * number of bytes matching startPosition
                 */
                Boolean validated = resumableUploadResourceManager.validateUpload(uploadId, totalByteToUpload,
                        startPosition, endPosition, totalFileSize);
                if (!validated) {
                    getResponse().setStatus(Status.CLIENT_ERROR_REQUESTED_RANGE_NOT_SATISFIABLE,
                            "Range parameter values not meets partial uploaded files size");
                    return;
                }
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
                getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage());
                return;
            }
        } else {
            // Clear previous file if exists
            resumableUploadResourceManager.clearUpload(uploadId);
        }

        /*
         * Start upload
         */
        Long writedBytes = resumableUploadResourceManager.handleUpload(uploadId, getRequest().getEntity(),
                startPosition);
        if (writedBytes < totalFileSize) {
            getResponse().setStatus(new Status(RESUME_INCOMPLETE.getCode()));
            Series<Parameter> headers = new Form();
            headers.add("Content-Length", "0");
            headers.add("Range", "0-" + (writedBytes - 1));
            getResponse().getAttributes().put("org.restlet.http.headers", headers);
        } else {
            String mappedPath;
            try {
                mappedPath = resumableUploadResourceManager.uploadDone(uploadId);
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
                getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage());
                return;
            }
            Representation output = new StringRepresentation(mappedPath, MediaType.TEXT_PLAIN);
            Response response = getResponse();
            response.setEntity(output);
            response.setStatus(Status.SUCCESS_OK);
        }

    }

    /**
     * GET request with uploadId is used to get the status of upload
     * If the upload is incomplete, the GET return the RANGE header attribute:</br>
     * Range: 0-{uploded end byte index}.
     */
    @Override
    public void handleGet() {
        String uploadId = RESTUtils.getAttribute(getRequest(), "uploadId");
        if (uploadId == null || uploadId.isEmpty()) {
            getResponse().setStatus(new Status(Status.CLIENT_ERROR_BAD_REQUEST, "Missing upload ID"));
            return;
        }

        try {
            if (!resumableUploadResourceManager.isUploadDone(uploadId)) {
                Long writedBytes = resumableUploadResourceManager.getWrittenBytes(uploadId);
                getResponse().setStatus(new Status(RESUME_INCOMPLETE.getCode()));
                Series<Parameter> headers = new Form();
                headers.add("Content-Length", "0");
                headers.add("Range", "0-" + (writedBytes - 1));
                getResponse().getAttributes().put("org.restlet.http.headers", headers);
            } else {
                Response response = getResponse();
                response.setStatus(Status.SUCCESS_OK);
            }
        } catch (IllegalStateException e) {
            getResponse().setStatus(new Status(Status.CLIENT_ERROR_NOT_FOUND, e.getMessage()));
            return;
        } catch (IOException e) {
            getResponse().setStatus(new Status(Status.SERVER_ERROR_INTERNAL, e.getMessage()));
            return;
        }
    }

    private Long getContentLength() {
        Long contentLength = 0L;
        Object oHeaders = getRequest().getAttributes().get("org.restlet.http.headers");
        if (oHeaders != null) {
            Series<Parameter> headers = (Series<Parameter>) oHeaders;
            Parameter contentLengthParam = headers.getFirst("Content-Length", true);
            if (contentLengthParam != null) {
                String contentLengthStr = contentLengthParam.getValue();
                if (!contentLengthStr.isEmpty() && StringUtils.isNumeric(contentLengthStr)) {
                    contentLength = Long.parseLong(contentLengthStr);
                }
            }
        }
        return contentLength;
    }

    private HeaderRange getHeaderRange() {
        HeaderRange headerRange = null;
        Object oHeaders = getRequest().getAttributes().get("org.restlet.http.headers");
        if (oHeaders != null) {
            Series<Parameter> headers = (Series<Parameter>) oHeaders;
            Parameter contentRangeParam = headers.getFirst("Content-Range", true);
            if (contentRangeParam != null) {
                String contentRangeStr = contentRangeParam.getValue();
                String range = contentRangeStr.substring(6);
                String[] rangeParts = range.split("/");
                Long startPosition = Long.parseLong(rangeParts[0].split("-")[0]);
                Long endPosition = Long.parseLong(rangeParts[0].split("-")[1]);
                Long totalFileSize = Long.parseLong(rangeParts[1]);
                headerRange = new HeaderRange(startPosition, endPosition, totalFileSize);
            }
        }
        return headerRange;
    }

    private class HeaderRange {
        public final NumberRange<Long> contentRange;

        public final Long totalFileSize;

        public HeaderRange(Long startPosition, Long endPosition, Long totalFileSize) {
            super();
            this.contentRange = new NumberRange<Long>(Long.class, startPosition, endPosition);
            this.totalFileSize = totalFileSize;
        }

        public Double getMinimum() {
            return contentRange.getMinimum();
        }

        public Double getMaximum() {
            return contentRange.getMaximum();
        }

        public Long getTotalFileSize() {
            return totalFileSize;
        }

        public Double getRange() {
            return (contentRange.getMaximum() - contentRange.getMinimum());
        }

    }

}