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