org.openstreetmap.josm.plugins.mapillary.oauth.UploadUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.openstreetmap.josm.plugins.mapillary.oauth.UploadUtils.java

Source

// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapillary.oauth;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;

import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.common.RationalNumber;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.plugins.mapillary.MapillaryAbstractImage;
import org.openstreetmap.josm.plugins.mapillary.MapillaryImportedImage;
import org.openstreetmap.josm.plugins.mapillary.MapillarySequence;
import org.openstreetmap.josm.plugins.mapillary.history.MapillaryRecord;
import org.openstreetmap.josm.plugins.mapillary.history.commands.CommandDelete;
import org.openstreetmap.josm.plugins.mapillary.utils.MapillaryUtils;
import org.openstreetmap.josm.plugins.mapillary.utils.PluginState;

/**
 * Upload utilities.
 *
 * @author nokutu
 */
public final class UploadUtils {
    /**
     * Required keys for POST
     */
    private static final String[] keys = { "key", "AWSAccessKeyId", "acl", "policy", "signature", "Content-Type" };

    /**
     * Mapillary upload URL
     */
    private static final String UPLOAD_URL = "https://s3-eu-west-1.amazonaws.com/mapillary.uploads.manual.images";

    /**
     * Count to name temporal files.
     */
    private static int c;

    private UploadUtils() {
        // Private constructor to avoid instantiation.
    }

    private static final class SequenceUploadThread extends Thread {
        private final Set<MapillaryAbstractImage> images;
        private final UUID uuid;
        private final boolean delete;
        private final ThreadPoolExecutor ex;

        private SequenceUploadThread(Set<MapillaryAbstractImage> images, boolean delete) {
            this.images = images;
            this.uuid = UUID.randomUUID();
            this.ex = new ThreadPoolExecutor(8, 8, 25, TimeUnit.SECONDS, new ArrayBlockingQueue<>(15));
            this.delete = delete;
        }

        @Override
        public void run() {
            PluginState.addImagesToUpload(this.images.size());
            MapillaryUtils.updateHelpText();
            for (MapillaryAbstractImage img : this.images) {
                if (!(img instanceof MapillaryImportedImage))
                    throw new IllegalArgumentException("The sequence contains downloaded images.");
                this.ex.execute(new SingleUploadThread((MapillaryImportedImage) img, this.uuid));
                while (this.ex.getQueue().remainingCapacity() == 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Main.error(e);
                    }
                }
            }
            this.ex.shutdown();
            try {
                this.ex.awaitTermination(15, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Main.error(e);
            }
            if (this.delete)
                MapillaryRecord.getInstance().addCommand(new CommandDelete(images));
        }
    }

    private static final class SingleUploadThread extends Thread {

        private final MapillaryImportedImage image;
        private final UUID uuid;

        private SingleUploadThread(MapillaryImportedImage image, UUID uuid) {
            this.image = image;
            this.uuid = uuid;
        }

        @Override
        public void run() {
            upload(this.image, this.uuid);
        }
    }

    /**
     * Returns a file containing the picture and an updated version of the EXIF
     * tags.
     *
     * @param image The image to be uploaded
     * @return A File object containing the picture and an updated version of the
     * EXIF tags.
     * @throws ImageReadException  if there are errors reading the image from the file.
     * @throws IOException         if there are errors getting the metadata from the file or writing
     *                             the output.
     * @throws ImageWriteException if there are errors writing the image in the file.
     */
    static File updateFile(MapillaryImportedImage image)
            throws ImageReadException, IOException, ImageWriteException {
        TiffOutputSet outputSet = null;
        TiffOutputDirectory exifDirectory;
        TiffOutputDirectory gpsDirectory;
        TiffOutputDirectory rootDirectory;

        // If the image is imported, loads the rest of the EXIF data.
        JpegImageMetadata jpegMetadata = null;
        try {
            ImageMetadata metadata = Imaging.getMetadata(image.getFile());
            jpegMetadata = (JpegImageMetadata) metadata;
        } catch (Exception e) {
            Main.warn(e);
        }

        if (null != jpegMetadata) {
            final TiffImageMetadata exif = jpegMetadata.getExif();
            if (null != exif) {
                outputSet = exif.getOutputSet();
            }
        }
        if (null == outputSet) {
            outputSet = new TiffOutputSet();
        }
        gpsDirectory = outputSet.getOrCreateGPSDirectory();
        exifDirectory = outputSet.getOrCreateExifDirectory();
        rootDirectory = outputSet.getOrCreateRootDirectory();

        gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF);
        gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF,
                GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION_REF_VALUE_TRUE_NORTH);

        gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION);
        gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION, RationalNumber.valueOf(image.getMovingCa()));

        exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL);
        exifDirectory.add(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL, image.getDate("yyyy/MM/dd HH:mm:ss"));

        // Removes the ImageDescription tag, that causes problems in the upload.
        rootDirectory.removeField(TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION);

        outputSet.setGPSInDegrees(image.getMovingLatLon().lon(), image.getMovingLatLon().lat());
        File tempFile = File.createTempFile("imagetoupload_" + c, ".tmp");
        c++;
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tempFile));

        // Transforms the image into a byte array.
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image.getImage(), "jpg", outputStream);
        byte[] imageBytes = outputStream.toByteArray();
        new ExifRewriter().updateExifMetadataLossless(imageBytes, os, outputSet);
        os.close();
        return tempFile;
    }

    /**
     * Uploads the given MapillaryImportedImage object.
     *
     * @param image The image to be uploaded
     * @throws IllegalStateException If {@link MapillaryUser#getSecrets()} returns null
     */
    public static void upload(MapillaryImportedImage image) {
        upload(image, UUID.randomUUID());
    }

    /**
     * @param image The image to be uploaded
     * @param uuid  The UUID used to create the sequence.
     */
    public static void upload(MapillaryImportedImage image, UUID uuid) {
        Map<String, String> secretMap = MapillaryUser.getSecrets();
        if (secretMap == null) {
            throw new IllegalStateException("Can't obtain secrents from user");
        }

        String key = MapillaryUser.getUsername() + '/' + uuid + '/' + image.getMovingLatLon().lat() + // TODO: Make sure, that the double values are not appended as something like "10e-4", "Infinity" or "NaN" (all possible values of Double.toString(double))
                '_' + image.getMovingLatLon().lon() + '_' + image.getMovingCa() + '_' + image.getCapturedAt()
                + ".jpg";

        String policy;
        String signature;
        policy = secretMap.get("images_policy");
        signature = secretMap.get("images_hash");

        Map<String, String> hash = new HashMap<>();
        hash.put("key", key);
        hash.put("AWSAccessKeyId", "AKIAI2X3BJAT2W75HILA");
        hash.put("acl", "private");
        hash.put("policy", policy);
        hash.put("signature", signature);
        hash.put("Content-Type", "image/jpeg");
        try {
            uploadFile(updateFile(image), hash);
        } catch (ImageReadException | ImageWriteException | IOException e) {
            Main.error(e);
        }
    }

    /**
     * @param file File that is going to be uploaded
     * @param hash Information attached to the upload
     * @throws IllegalArgumentException if the hash doesn't contain all the needed keys.
     */
    private static void uploadFile(File file, Map<String, String> hash) throws IOException {
        HttpClientBuilder builder = HttpClientBuilder.create();
        HttpPost httpPost = new HttpPost(UPLOAD_URL);

        try (CloseableHttpClient httpClient = builder.build()) {
            MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
            for (String key : keys) {
                if (hash.get(key) == null)
                    throw new IllegalArgumentException();
                entityBuilder.addPart(key, new StringBody(hash.get(key), ContentType.TEXT_PLAIN));
            }
            entityBuilder.addPart("file", new FileBody(file));
            HttpEntity entity = entityBuilder.build();
            httpPost.setEntity(entity);
            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
                if (response.getStatusLine().toString().contains("204")) {
                    PluginState.imageUploaded();
                    Main.info(PluginState.getUploadString() + " (Mapillary)");
                } else {
                    Main.info("Upload error");
                }
            }
        }
        if (!file.delete()) {
            Main.error("MapillaryPlugin: File could not be deleted during upload");
        }
        MapillaryUtils.updateHelpText();
    }

    /**
     * Uploads the given {@link MapillarySequence}.
     *
     * @param sequence The sequence to upload. It must contain only
     *                 {@link MapillaryImportedImage} objects.
     * @param delete   Whether the images must be deleted after upload or not.
     */
    public static void uploadSequence(MapillarySequence sequence, boolean delete) {
        Main.worker.submit(new SequenceUploadThread(new ConcurrentSkipListSet<>(sequence.getImages()), delete));
    }
}