org.eclipse.orion.internal.server.servlets.xfer.ClientImport.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.orion.internal.server.servlets.xfer.ClientImport.java

Source

/*******************************************************************************
 * Copyright (c) 2011, 2012 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.orion.internal.server.servlets.xfer;

import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;
import java.util.zip.*;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.*;
import org.eclipse.orion.internal.server.core.IOUtilities;
import org.eclipse.orion.internal.server.servlets.ProtocolConstants;
import org.eclipse.orion.internal.server.servlets.ServletResourceHandler;
import org.eclipse.orion.internal.server.servlets.file.NewFileServlet;
import org.eclipse.orion.server.core.ServerStatus;
import org.eclipse.osgi.util.NLS;
import org.json.JSONObject;
import org.osgi.framework.FrameworkUtil;

/**
 * Represents an import from client operation in progress.
 */
class ClientImport {

    private static final String FILE_DATA = "xfer.data"; //$NON-NLS-1$
    private static final String FILE_INDEX = "xfer.properties"; //$NON-NLS-1$
    private static final String KEY_FILE_NAME = "FileName"; //$NON-NLS-1$
    private static final String KEY_LENGTH = "Length"; //$NON-NLS-1$
    private static final String KEY_OPTIONS = "Options"; //$NON-NLS-1$
    private static final String KEY_PATH = "Path"; //$NON-NLS-1$
    private static final String KEY_TRANSFERRED = "Transferred"; //$NON-NLS-1$
    private static final String KEY_SOURCE_URL = "SourceURL"; //$NON-NLS-1$

    /**
     * The UUID of this import operation.
     */
    private final String id;

    private Properties props = new Properties();
    private final ServletResourceHandler<IStatus> statusHandler;

    /**
     * Creates a new import. This may represent an import that has not yet started,
     * or one that is already underway.
     */
    ClientImport(String id, ServletResourceHandler<IStatus> servletResourceHandler) throws IOException {
        this.id = id;
        this.statusHandler = servletResourceHandler;
        restore();
    }

    /**
     * Completes a move after a file transfer. Returns <code>true</code> if the move was
     * successful, and <code>false</code> otherwise. In case of failure, this method
     * handles setting an appropriate response.
     */
    private boolean completeMove(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
        IPath destPath = new Path(getPath()).append(getFileName());
        try {
            IFileStore source = EFS.getStore(new File(getStorageDirectory(), FILE_DATA).toURI());
            IFileStore destination = NewFileServlet.getFileStore(req, destPath);
            source.move(destination, EFS.OVERWRITE, null);
        } catch (CoreException e) {
            String msg = NLS.bind("Failed to complete file transfer on {0}", destPath.toString());
            statusHandler.handleRequest(req, resp,
                    new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, e));
            return false;
        }
        return true;
    }

    /**
     * We have just received the final chunk of data for a file upload.
     * Complete the transfer by moving the uploaded content into the
     * workspace.
     * @throws IOException 
     */
    private void completeTransfer(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        List<String> options = getOptions();
        boolean success;
        if (!options.contains("raw")) { //$NON-NLS-1$
            success = completeUnzip(req, resp);
        } else {
            success = completeMove(req, resp);
        }
        if (success) {
            resp.setHeader(ProtocolConstants.HEADER_LOCATION, req.getContextPath() + "/file" + getPath()); //$NON-NLS-1$
            resp.setStatus(HttpServletResponse.SC_CREATED);
            resp.setContentType(ProtocolConstants.CONTENT_TYPE_HTML);
        }
    }

    private static boolean hasExcludedParent(IFileStore destination, IFileStore destinationRoot,
            List<String> excludedFiles) {
        if (destination.equals(destinationRoot)) {
            return false;
        }
        if (excludedFiles.contains(destination.getName())) {
            return true;
        }
        return hasExcludedParent(destination.getParent(), destinationRoot, excludedFiles);
    }

    /**
     * Unzips the transferred file. Returns <code>true</code> if the unzip was
     * successful, and <code>false</code> otherwise. In case of failure, this method
     * handles setting an appropriate response.
     */
    private boolean completeUnzip(HttpServletRequest req, HttpServletResponse resp) throws ServletException {
        IPath destPath = new Path(getPath());
        boolean force = false;
        List<String> filesFailed = new ArrayList<String>();
        if (req.getParameter("force") != null) {
            force = req.getParameter("force").equals("true");
        }
        List<String> excludedFiles = new ArrayList<String>();
        if (req.getParameter(ProtocolConstants.PARAM_EXCLUDE) != null) {
            excludedFiles = Arrays.asList(req.getParameter(ProtocolConstants.PARAM_EXCLUDE).split(","));
        }
        try {
            ZipFile source = new ZipFile(new File(getStorageDirectory(), FILE_DATA));
            IFileStore destinationRoot = NewFileServlet.getFileStore(req, destPath);
            Enumeration<? extends ZipEntry> entries = source.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                IFileStore destination = destinationRoot.getChild(entry.getName());
                if (!destinationRoot.isParentOf(destination)
                        || hasExcludedParent(destination, destinationRoot, excludedFiles)) {
                    //file should not be imported
                    continue;
                }
                if (entry.isDirectory())
                    destination.mkdir(EFS.NONE, null);
                else {
                    if (!force && destination.fetchInfo().exists()) {
                        filesFailed.add(entry.getName());
                        continue;
                    }
                    destination.getParent().mkdir(EFS.NONE, null);
                    // this filter will throw an IOException if a zip entry is larger than 100MB
                    FilterInputStream maxBytesReadInputStream = new FilterInputStream(
                            source.getInputStream(entry)) {
                        private static final int maxBytes = 0x6400000; // 100MB
                        private int totalBytes;

                        private void addByteCount(int count) throws IOException {
                            totalBytes += count;
                            if (totalBytes > maxBytes) {
                                throw new IOException("Zip file entry too large");
                            }
                        }

                        @Override
                        public int read() throws IOException {
                            int c = super.read();
                            if (c != -1) {
                                addByteCount(1);
                            }
                            return c;
                        }

                        @Override
                        public int read(byte[] b, int off, int len) throws IOException {
                            int read = super.read(b, off, len);
                            if (read != -1) {
                                addByteCount(read);
                            }
                            return read;
                        }
                    };
                    boolean fileWritten = false;
                    try {
                        IOUtilities.pipe(maxBytesReadInputStream, destination.openOutputStream(EFS.NONE, null),
                                false, true);
                        fileWritten = true;
                    } finally {
                        if (!fileWritten) {
                            try {
                                destination.delete(EFS.NONE, null);
                            } catch (CoreException ce) {
                                // best effort
                            }
                        }
                    }
                }
            }
            source.close();

            if (!filesFailed.isEmpty()) {
                String failedFilesList = "";
                for (String file : filesFailed) {
                    if (failedFilesList.length() > 0) {
                        failedFilesList += ", ";
                    }
                    failedFilesList += file;
                }
                String msg = NLS.bind(
                        "Failed to transfer all files to {0}, the following files could not be overwritten {1}",
                        destPath.toString(), failedFilesList);
                JSONObject jsonData = new JSONObject();
                jsonData.put("ExistingFiles", filesFailed);
                statusHandler.handleRequest(req, resp,
                        new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, msg, jsonData, null));
                return false;
            }

        } catch (ZipException e) {
            //zip exception implies client sent us invalid input
            String msg = NLS.bind("Failed to complete file transfer on {0}", destPath.toString());
            statusHandler.handleRequest(req, resp,
                    new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, msg, e));
            return false;
        } catch (Exception e) {
            //other failures should be considered server errors
            String msg = NLS.bind("Failed to complete file transfer on {0}", destPath.toString());
            statusHandler.handleRequest(req, resp,
                    new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, e));
            return false;
        }
        return true;
    }

    /**
     * A post operation represents the beginning of an import operation. This method
     * initializes the import and sets an appropriate response.
     */
    void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        save();
        //if a source URl is specified then we are importing from remote URL
        if (getSourceURL() != null) {
            doImportFromURL(req, resp);
            return;
        }
        //if the transfer length header is not specified, then the file is being uploaded during the POST
        if (req.getHeader(ProtocolConstants.HEADER_XFER_LENGTH) == null) {
            doPut(req, resp);
            return;
        }
        //otherwise the POST is just starting a transfer to be completed later
        resp.setStatus(HttpServletResponse.SC_OK);
        setResponseLocationHeader(req, resp);
    }

    private void doImportFromURL(HttpServletRequest req, HttpServletResponse resp)
            throws IOException, ServletException {
        URL source = getSourceURL();
        IOUtilities.pipe(source.openStream(),
                new FileOutputStream(new File(getStorageDirectory(), FILE_DATA), true), true, true);
        completeTransfer(req, resp);
    }

    private void setResponseLocationHeader(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException {
        URI requestURI = ServletResourceHandler.getURI(req);
        String responsePath = "/" + new Path(requestURI.getPath()).segment(0) + "/import/" + id; //$NON-NLS-1$ //$NON-NLS-2$
        URI responseURI;
        try {
            responseURI = new URI(requestURI.getScheme(), requestURI.getAuthority(), responsePath, null, null);
        } catch (URISyntaxException e) {
            //should not be possible
            throw new ServletException(e);
        }
        resp.setHeader(ProtocolConstants.HEADER_LOCATION,
                ServletResourceHandler.resovleOrionURI(req, responseURI).toString());
    }

    /**
     * A put is used to send a chunk of a file.
     */
    void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int transferred = getTransferred();
        int length = getLength();
        int headerLength = Integer.valueOf(req.getHeader(ProtocolConstants.HEADER_CONTENT_LENGTH));
        String rangeString = req.getHeader(ProtocolConstants.HEADER_CONTENT_RANGE);
        if (rangeString == null)
            rangeString = "bytes 0-" + (length - 1) + '/' + length; //$NON-NLS-1$
        ContentRange range = ContentRange.parse(rangeString);
        if (length != range.getLength()) {
            fail(req, resp, "Chunk specifies an incorrect document length");
            return;
        }
        if (range.getStartByte() > transferred) {
            fail(req, resp, "Chunk missing; Expected start byte: " + transferred);
            return;
        }
        if (range.getEndByte() < range.getStartByte()) {
            fail(req, resp, "Invalid range: " + rangeString);
            return;
        }
        int chunkSize = 1 + range.getEndByte() - range.getStartByte();
        if (chunkSize != headerLength) {
            fail(req, resp, "Content-Range doesn't agree with Content-Length");
            return;
        }
        byte[] chunk = readChunk(req, chunkSize);
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(new File(getStorageDirectory(), FILE_DATA), true);
            FileChannel channel = fout.getChannel();
            channel.position(range.getStartByte());
            channel.write(ByteBuffer.wrap(chunk));
            channel.close();
        } finally {
            try {
                if (fout != null)
                    fout.close();
            } catch (IOException e) {
                //ignore secondary failure
            }
        }
        transferred = range.getEndByte() + 1;
        setTransferred(transferred);
        save();
        if (transferred >= length) {
            completeTransfer(req, resp);
            return;
        }
        resp.setStatus(308);//Resume Incomplete
        resp.setHeader("Range", "bytes 0-" + range.getEndByte()); //$NON-NLS-2$
        setResponseLocationHeader(req, resp);
    }

    /**
     * Reads the chunk of data to be imported from the request's input stream.
     */
    private byte[] readChunk(HttpServletRequest req, int chunkSize) throws IOException {
        ServletInputStream requestStream = req.getInputStream();
        String contentType = req.getHeader(ProtocolConstants.HEADER_CONTENT_TYPE);
        if (contentType != null && contentType.startsWith("multipart")) //$NON-NLS-1$
            return readMultiPartChunk(requestStream, contentType);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(chunkSize);
        IOUtilities.pipe(requestStream, outputStream, false, false);
        return outputStream.toByteArray();
    }

    private byte[] readMultiPartChunk(ServletInputStream requestStream, String contentType) throws IOException {
        //fast forward stream past multi-part header
        int boundaryOff = contentType.indexOf("boundary="); //$NON-NLS-1$
        String boundary = contentType.substring(boundaryOff + 9);
        BufferedReader reader = new BufferedReader(new InputStreamReader(requestStream, "ISO-8859-1")); //$NON-NLS-1$
        StringBuffer out = new StringBuffer();
        //skip headers up to the first blank line
        String line = reader.readLine();
        while (line != null && line.length() > 0)
            line = reader.readLine();
        //now process the file

        char[] buf = new char[1000];
        int read;
        while ((read = reader.read(buf)) > 0) {
            out.append(buf, 0, read);
        }
        //remove the boundary from the output (end of input is \r\n--<boundary>--\r\n)
        out.setLength(out.length() - (boundary.length() + 8));
        return out.toString().getBytes("ISO-8859-1"); //$NON-NLS-1$
    }

    private void fail(HttpServletRequest req, HttpServletResponse resp, String msg) throws ServletException {
        statusHandler.handleRequest(req, resp,
                new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_BAD_REQUEST, msg, null));
    }

    private String getFileName() {
        return props.getProperty(KEY_FILE_NAME, ""); //$NON-NLS-1$
    }

    private int getLength() {
        return Integer.valueOf(props.getProperty(KEY_LENGTH, "0")); //$NON-NLS-1$
    }

    private List<String> getOptions() {
        return TransferServlet.getOptions(props.getProperty(KEY_OPTIONS, ""));//$NON-NLS-1$
    }

    private String getPath() {
        return props.getProperty(KEY_PATH, ""); //$NON-NLS-1$
    }

    private URL getSourceURL() {
        String urlString = props.getProperty(KEY_SOURCE_URL, null);
        try {
            return urlString == null ? null : new URL(urlString);
        } catch (MalformedURLException e) {
            //impossible because we validated URL in the setter method
            throw new RuntimeException(e);
        }
    }

    private File getStorageDirectory() {
        return FrameworkUtil.getBundle(ClientImport.class).getDataFile("xfer/" + id); //$NON-NLS-1$
    }

    /**
     * Returns the number of bytes transferred so far.
     */
    private Integer getTransferred() {
        return Integer.valueOf(props.getProperty(KEY_TRANSFERRED, "0")); //$NON-NLS-1$
    }

    /**
     * Load any progress information for the import so far.
     */
    void restore() throws IOException {
        try {
            File dir = getStorageDirectory();
            File index = new File(dir, FILE_INDEX);
            props.load(new FileInputStream(index));
        } catch (FileNotFoundException e) {
            //ok if file doesn't exist yet
        }
    }

    void save() throws IOException {
        File dir = getStorageDirectory();
        dir.mkdirs();
        File index = new File(dir, FILE_INDEX);
        props.store(new FileOutputStream(index), null);
    }

    public void setFileName(String name) {
        props.put(KEY_FILE_NAME, name == null ? "" : name); //$NON-NLS-1$
    }

    /**
     * Sets the total length of the file being imported.
     */
    public void setLength(long length) {
        props.put(KEY_LENGTH, Long.toString(length));
    }

    public void setOptions(String options) {
        props.put(KEY_OPTIONS, options == null ? "" : options); //$NON-NLS-1$
    }

    /**
     * Sets the path of the file in the workspace once the import completes.
     */
    public void setPath(IPath path) {
        props.put(KEY_PATH, path.toString());
    }

    private void setTransferred(int transferred) {
        props.put(KEY_TRANSFERRED, Integer.toString(transferred));
    }

    /**
     * Sets the URL of the file to be imported (optional).
     */
    public void setSourceURL(String urlString) throws MalformedURLException {
        //ensure it is a valid absolute URL
        if (new URL(urlString).getProtocol() == null)
            throw new MalformedURLException(NLS.bind("Expected an absolute URI: {0}", urlString));
        props.put(KEY_SOURCE_URL, urlString);
    }

}