typicalnerd.musicarchive.client.network.FileUploader.java Source code

Java tutorial

Introduction

Here is the source code for typicalnerd.musicarchive.client.network.FileUploader.java

Source

/**
 * Copyright 2017 Robert Lohr
 * 
 * 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.
 * 
 * File created: 11.02.2017
 */
package typicalnerd.musicarchive.client.network;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownServiceException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.commons.io.IOUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

import typicalnerd.musicarchive.client.metadata.TrackMetaData;

/**
 * Uploads a file to the MusicArchive web service.
 * 
 * @author Robert Lohr
 * @since 1.0.0
 */
public class FileUploader {

    private String baseUrl;
    private ObjectMapper mapper;

    /**
     * Create a new file uploader that uploads to the given URL.
     * 
     * @param baseUrl
     * The base URL of the web service. It's used to construct the upload URLs by appending
     * relative paths.
     */
    public FileUploader(String baseUrl) {
        this.baseUrl = baseUrl;
        this.mapper = new ObjectMapper();
    }

    /**
     * Performs the upload of a file from the file system and the associated meta data in
     * JSON format.
     * 
     * @param file
     * The file's location in the file system.
     * 
     * @param metaData
     * The JSON meta data that contains artist, album and the like.
     * 
     * @see FileUploader#uploadMultipart(Path, TrackMetaData)
     */
    public void upload(Path file, TrackMetaData metaData) {
        // The upload is split into two parts:
        // 1) Upload the naked file.
        // 2) Add meta data to the file by using the URL that is returned in the process.
        // 
        // This way we can make use of existing functionality to edit meta data. It is 
        // necessary to add/update/remove/get functionality for meta data any way so it
        // makes sense to use this for the upload as well. The only downside: to upload
        // a file we have to make two requests.

        // There is another option to bundle these two steps into one multi-part upload.
        // See uploadMultipart(Path, TrackMetaData) for more information.
        try {
            FileUploadResult fileResult = uploadFile(file);

            if (HttpURLConnection.HTTP_OK == fileResult.getStatusCode()) {
                // Darn it: Check for existing meta data.
                if (haveMetaData(fileResult.getMetaUrl())) {
                    // Nothing to do.
                    return;
                }
            }

            /*MetaUploadResult metaUploadResult = */uploadMeta(metaData, fileResult);
            // TODO Think of what we could do with the result.
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // TODO Add a more elaborate error handling than just writing to the console.
        // This is a task left for the reader as it does not affect the purpose of this 
        // sample application and is therefore left out for brevity (and maybe lazyness ;-) )
    }

    /**
     * Uploads the file to the web service.
     * 
     * @param file
     * The file's location in the file system.
     * 
     * @return
     * Returns the {@link FileUploadResult} in case of a successful upload.
     * 
     * @throws IOException
     */
    private FileUploadResult uploadFile(Path file) throws IOException {
        URL url = new URL(baseUrl + "/files");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/octet-stream");
        connection.setRequestProperty("Content-Length", String.valueOf(Files.size(file)));
        connection.setRequestProperty("Accept", "application/json");
        connection.setDoOutput(true);
        connection.setDoInput(true);

        try (InputStream in = new BufferedInputStream(new FileInputStream(file.toFile()))) {
            try (OutputStream out = connection.getOutputStream()) {
                // Lazy as we are, we use one of the many nice functions in the Apache
                // Commons library.
                IOUtils.copy(in, out);
            }
        }

        // Now it's time to read the first result. If all goes well, this was a 
        // HTTP 201 - CREATED. If the file is already known, e.g. because hash and
        // name match an existing object, then HTTP 200 is returned and nothing was
        // changed on the server. In that case we query if there is meta data and if
        // so skip the upload in with the assumption that the latest version is already 
        // on the server. If not, we continue as planned.
        connection.connect();
        int result = connection.getResponseCode();

        if (200 != result || 201 != result) {
            try (InputStream in = connection.getInputStream()) {
                ErrorResponse e = mapper.readValue(in, ErrorResponse.class);
                throw new UnknownServiceException("Upload of file failed. " + e.getMessage());
            }
        }

        // Parse the response to get the location of where to put the meta data.
        // We expect a JSON response so let Jackson parse it into an expected response
        // object.
        FileUploadResult uploadResult = new FileUploadResult(result);
        try (InputStream in = connection.getInputStream()) {
            ObjectReader reader = mapper.readerForUpdating(uploadResult);
            reader.readValue(in);
        }
        return uploadResult;
    }

    /**
     * Upload a file's meta data to the web service.
     * 
     * @param metaData
     * The meta data of the file.
     * 
     * @param uploadResult
     * The result of the file upload. This contains the location where to POST the meta
     * data.
     * 
     * @return
     * Returns the {@link MetaUploadResult} in case of a successful upload.
     * 
     * @throws IOException
     */
    private MetaUploadResult uploadMeta(TrackMetaData metaData, FileUploadResult uploadResult) throws IOException {
        // This time we need to turn things around and prepare the data first. Only then
        // can we specify the "Content-Length" header of the request. It is not necessary
        // per se, but it makes us a good citizen if we can tell the server how big the
        // data will be. In theory this would allow to calculate an ETA and other stats
        // that are not very relevant for this small data.
        String json = mapper.writeValueAsString(metaData);
        byte[] binary = json.getBytes(StandardCharsets.UTF_8);

        // Now we can do the URL setup dance.
        URL url = new URL(baseUrl + uploadResult.getMetaUrl());
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Content-Length", String.valueOf(json.length()));
        connection.setRequestProperty("Accept", "application/json");
        connection.setDoOutput(true);
        connection.setDoInput(true);

        try (ByteArrayInputStream in = new ByteArrayInputStream(binary)) {
            try (OutputStream out = connection.getOutputStream()) {
                // Still lazy ;-)
                IOUtils.copy(in, out);
            }
        }

        connection.connect();
        int result = connection.getResponseCode();

        if (HttpURLConnection.HTTP_CREATED != result) {
            try (InputStream in = connection.getInputStream()) {
                ErrorResponse e = mapper.readValue(in, ErrorResponse.class);
                throw new UnknownServiceException("Upload of meta data failed. " + e.getMessage());
            }
        }

        // Fanfare! We're done.
        return new MetaUploadResult(result);
    }

    /**
     * Queries the web service with the given URL to confirm whether a file already has
     * meta data associated with it or not.
     * 
     * @param metaUrl
     * The URL to query as returned from the web service when uploading a file.
     * 
     * @return
     * <code>true</code> if meta data exists at the given location or <code>false</code> if
     * not.
     * 
     * @throws IOException 
     */
    private boolean haveMetaData(String metaUrl) throws IOException {
        URL url = new URL(baseUrl + metaUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Accept", "application/json");
        connection.connect();
        return HttpURLConnection.HTTP_OK == connection.getResponseCode();
    }

    /**
     * Performs the upload of a file from the file system and the associated meta data in
     * JSON format. This achieves the same result as {@link FileUploader#upload(Path, TrackMetaData)}
     * but only uses one request using a multi-part upload. The server needs to handle
     * this form of upload separately.
     * 
     * @param file
     * The file's location in the file system.
     * 
     * @param metaData
     * The JSON meta data that contains artist, album and the like.
     * 
     * @see FileUploader#upload(Path, TrackMetaData)
     */
    public void uploadMultipart(Path file, TrackMetaData metaData) {
        // TODO Add code for demonstration purposes.
    }
}