All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fiftyone.mobile.detection.AutoUpdate Maven / Gradle / Ivy

The newest version!
package fiftyone.mobile.detection;

import fiftyone.mobile.detection.factories.StreamFactory;
import fiftyone.properties.DetectionConstants;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.zip.DataFormatException;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HttpsURLConnection;

/* *********************************************************************
 * This Source Code Form is copyright of 51Degrees Mobile Experts Limited. 
 * Copyright © 2014 51Degrees Mobile Experts Limited, 5 Charlotte Close,
 * Caversham, Reading, Berkshire, United Kingdom RG4 7BY
 * 
 * This Source Code Form is the subject of the following patent 
 * applications, owned by 51Degrees Mobile Experts Limited of 5 Charlotte
 * Close, Caversham, Reading, Berkshire, United Kingdom RG4 7BY: 
 * European Patent Application No. 13192291.6; and
 * United States Patent Application Nos. 14/085,223 and 14/085,301.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.
 * 
 * If a copy of the MPL was not distributed with this file, You can obtain
 * one at http://mozilla.org/MPL/2.0/.
 * 
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
 * ********************************************************************* */

/*
 * Used to fetch new device data from 51Degrees.com if a Premium or Enterprise 
 * licence has been installed.
 */
public class AutoUpdate {

    //Path to the compressed data file.
    private static String compressedTempFile = "";
    //Path to the uncompressed data file.
    private static String uncompressedTempFile = "";
    
    /**
     * Implements the main update logic. First the paths to temporary data files 
     * are initialised. Then a request is made to 51Degrees.com to check for an 
     * updated data file. One of the request headers is set to the last-modified 
     * date of the data file (if any). If the local data file is already of the 
     * latest version, then a 304 header 'Not Modified' is returned. Otherwise 
     * the file is downloaded in to a temporary location and uncompressed. The 
     * temporary file is then deleted. New data file is then validated and a 
     * check is carried out to determine if the old data file needs to be 
     * replaced. Finally, if the data file is replaced if required.
     *
     * @returns True if all stages completed successfully, False otherwise.
     * @param dataFilePath string representing path to 51Degrees data file.
     * @param licenseKeys An array of licence keys with at least one entry 
     * represented as strings.
     */
    private static boolean getNewDataset(final String[] licenseKeys, 
                final String dataFilePath) throws AutoUpdateException {
        try {
            //Initialize paths to temporary files.
            initTempFiles(dataFilePath);
            // Try to get the date the data was last modified. No existent files
            // or lite data do not need dates.
            final File oldDataFile = new File(dataFilePath);
            long lastModified = -1;
            if (oldDataFile.exists()) {
                final Dataset oldDataset = StreamFactory.create(dataFilePath, false);
                if (!oldDataset.getName().contains("Lite")) {
                    lastModified = oldDataFile.lastModified();
                }
                oldDataset.dispose();
            }
            System.gc();
            // Download the data to the temporary data file.
            if (!download(licenseKeys, lastModified, compressedTempFile)) {
                throw new AutoUpdateException("Download failed");
            }
            //Decompress data.
            decompressData(compressedTempFile, uncompressedTempFile);
            //Delete compressed file.
            File compressedFile = new File(compressedTempFile);
            if (compressedFile.delete() == false) {
                Logger.getLogger(AutoUpdate.class.getName()).log(Level.WARNING,
                            "Compressed file downloaded from 51Degrees.com "
                                    + "could not be deleted.");
            }
            // Create a dataset and load the data in.
            final Dataset newDataSet = StreamFactory.create(uncompressedTempFile, true);
            //Test the new data and check if old one needs to be replaced.
            boolean copyFile = true;
            final File dataFile = new File(dataFilePath);
            // Confirm the new data is newer than current.
            if (dataFile.exists()) {
                final Dataset currentDataSet = StreamFactory.create(dataFilePath, false);
                copyFile = newDataSet.published.getTime() > currentDataSet.published.getTime() || 
                        !newDataSet.getName().equals(currentDataSet.getName());

                currentDataSet.dispose();
            }
            newDataSet.dispose();
            System.gc();
            //If the downloaded file is either newer, or has a different name.
            if (copyFile) {
                //Copy new file re-writing the contents of the current.
                File source = new File(uncompressedTempFile);
                File destination = new File(dataFilePath);
                
                boolean moved = source.renameTo(destination);
                if (!moved) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Could not replace master data file with new one. ");
                    sb.append("Please verify the master data file is not used ");
                    sb.append("elsewhere in your program.");
                    Logger.getLogger(AutoUpdate.class.getName()).log(Level.WARNING,
                            sb.toString());
                    return false;
                }
                
                /* 
                 *  Section below is commented out as it is not compatible with 
                 *  JDK 1.6. If you are rebuilding the JAR for use with 1.7 or 
                 *  above, then feel free to use the commented section instead 
                 *  of the above copy procedure.
                 */
                
                /*
                Path source = Paths.get(uncompressedTempFile);
                Path destination = Paths.get(dataFilePath);
                try {
                    Files.copy(source, destination, REPLACE_EXISTING);
                    Files.
                } catch (IOException ex) {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Could not replace master data file with new one. ");
                    sb.append("Please verify the master data file is not used ");
                    sb.append("elsewhere in your program.");
                    Logger.getLogger(AutoUpdate.class.getName()).log(Level.WARNING,
                            sb.toString());
                    return false;
                }
                */
                source = null;
                destination = null;
                //Try to delete temp file.
                File tempMasterFile = new File(uncompressedTempFile);
                int count = 5;
                while (!tempMasterFile.delete()) {
                    if (count <= 0) {
                        StringBuilder sb = new StringBuilder();
                        sb.append("Failed to delete the uncompressed temporary ");
                        sb.append("data file.");
                        throw new AutoUpdateException(sb.toString());
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
                    } finally {
                        count--;
                    }
                }
                return true;
            } else {
                //No need to update. File names are the same. Dates of both 
                //files do not indicate an update is required.
                Logger.getLogger(AutoUpdate.class.getName()).log(Level.INFO,"Data file is already up to date.");
                File f = new File(uncompressedTempFile);
                if (f.exists())
                    f.delete();
                return false;
            }
        } catch (IOException ex) {
            throw new AutoUpdateException(String.format(
                    "Exception reading data stream from server '%s'.",
                    DetectionConstants.AUTO_UPDATE_URL) + ex.getMessage());
        } catch (DataFormatException ex) {
            Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
        }
        return false;
    }

    /**
     *
     * Calculates the MD5 hash of the given data array.
     *
     * @param pathToFile calculate md5 of this file.
     * @return The MD5 hash of the given data.
     */
    private static String getMd5Hash(String pathToFile) {
        FileInputStream fis = null;
        MessageDigest md5 = null;
        try {
            //Allocate resources.
            fis = new FileInputStream(pathToFile);
            md5 = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[2048];
            int bytesRead = -1;

            //Get the md5 and format as a string.
            while((bytesRead = fis.read(buffer)) != -1) {
                md5.update(buffer, 0, bytesRead);
            }
            byte[] md5Bytes = md5.digest();
            StringBuilder hashBuilder = new StringBuilder();
            for (int i = 0; i < md5Bytes.length; i++) {
                hashBuilder.append(String.format("%02X ", md5Bytes[i]));
            }
            
            //Release resources.
            fis.close();
            md5Bytes = null;
            buffer = null;
            
            // The hash retrived from the responce header is in lower case with 
            // no spaces, must make sure this hash conforms to the scheme too.
            return hashBuilder.toString().toLowerCase().replaceAll(" ", "");
        } catch (FileNotFoundException ex) {
            Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } catch (IOException ex) {
            Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } finally {
            //Release FileInputStream
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ex) {
                    Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            //Release MD5
            if (md5 != null) {
                md5 = null;
            }
        }
    }

    /**
     *
     * Verifies that the data has been downloaded correctly by comparing an MD5
     * hash off the downloaded data with one taken before the data was sent,
     * which is stored in a response header.
     *
     * @param client The Premium data download connection.
     * @param pathToFile path to compressed data file that has been downloaded.
     * @return True if the hashes match, else false.
     */
    private static boolean validateMD5(
            final HttpURLConnection client,
            String pathToFile) {
        final String serverHash = client.getHeaderField("Content-MD5");
        final String downloadHash = getMd5Hash(pathToFile);
        return serverHash != null && serverHash.equals(downloadHash);
    }

    /**
     * Method joins given number of strings separating each by the specified 
     * separator. Used to construct the update URL.
     * @param seperator what separates the strings.
     * @param strings strings to join.
     * @return all of the strings combined in to one and separated by separator.
     */
    private static String joinString(final String seperator, final String[] strings) {
        final StringBuilder sb = new StringBuilder();
        int size = strings.length;
        for (int i = 0; i < size; i++) {
            sb.append(strings[i]);
            if (i < size - 1) {
                sb.append(seperator);
            }
        }
        return sb.toString();
    }

    /**
     * Constructs the URL needed to download Premium data.
     *
     * @param licenseKeys Array of licence key strings.
     * @return Premium data download URL.
     * @throws MalformedURLException
     */
    private static URL fullUrl(String[] licenseKeys) throws MalformedURLException {

        final String[] parameters = {
            "LicenseKeys=" + joinString("|", licenseKeys),
            "Download=True",
            "Type=BinaryV3"};
        String url = String.format("%s?%s", DetectionConstants.AUTO_UPDATE_URL, 
                joinString("&", parameters));
        return new URL(url);
    }

    /**
     * Uses the given license key to perform a device data update, writing the
     * data to the file system and filling providers from this factory instance
     * with it.
     *
     * @param licenseKey the licence key to submit to the server
     * @param dataFilePath path to the device data file
     * @return true for a successful update. False can indicate that data was
     * unavailable, corrupt, older than the current data or not enough memory
     * was available. In that case the current data is used.
     * @throws AutoUpdateException exception detailing problem during the update
     */
    public static boolean update(final String licenseKey, String dataFilePath) 
            throws AutoUpdateException {
        return update(new String[]{licenseKey}, dataFilePath);
    }

    /**
     * Uses the given license key to perform a device data update. This method 
     * allows you to specify the location of the original data file as well as 
     * the two temporary data files used to store the data at intermediate 
     * stages of the update.
     * 
     * @param licenseKey the licence key to use for the update request.
     * @param dataFilePath where the original data file is located.
     * @param compressedFile where the compressed data file should be located.
     * @param uncompressedFile where the uncompressed data file should be located.
     * @return True if update was successful, False otherwise.
     * @throws AutoUpdateException 
     */
    public static boolean update(final String licenseKey, String dataFilePath, 
            String compressedFile, String uncompressedFile) throws AutoUpdateException {
        compressedTempFile = compressedFile;
        uncompressedTempFile = uncompressedFile;
        return update(new String[]{licenseKey}, dataFilePath);
    }
    
    /**
     * Uses the given license key to perform a device data update, writing the
     * data to the file system and filling providers from this factory instance
     * with it.
     *
     * @param licenseKeys the licence keys to submit to the server
     * @param dataFilePath path to device data file
     * @return true for a successful update. False can indicate that data was
     * unavailable, corrupt, older than the current data or not enough memory
     * was available. In that case the current data is used.
     * @throws AutoUpdateException exception detailing problem during the update
     */
    public static boolean update(final String[] licenseKeys, String dataFilePath) 
            throws AutoUpdateException {

        if (licenseKeys == null || licenseKeys.length == 0) {
            throw new AutoUpdateException(
                    "Device data cannot be updated without a licence key.");
        }

        // If a valid license key exists then proceed
        final String[] validKeys = getValidKeys(licenseKeys);
        if (validKeys.length > 0) {
            // Download and verify the data. Return the result.
            return getNewDataset(validKeys, dataFilePath);
        } else {
            throw new AutoUpdateException(
                    "The license key(s) provided were invalid.");
        }
    }

    /**
     * Validate the supplied keys to exclude keys from 3rd party products from 
     * being used.
     * @param licenseKeys an array of licence key strings to validate.
     * @return an array of valid licence keys.
     */
    private static String[] getValidKeys(final String[] licenseKeys) {
        final List validKeys = new ArrayList();
        for (String key : licenseKeys) {
            final Matcher m = DetectionConstants.LICENSE_KEY_VALIDATION_REGEX.matcher(key);
            if (m.matches()) {
                validKeys.add(key);
            }
        }
        return validKeys.toArray(new String[validKeys.size()]);
    }

    /**
     * Downloads and validates data, returning a byte array or null if download
     * or validation was unsuccessful.
     *
     * @param licenseKeys an array of keys to fetch a new data file with.
     * @return a decompressed byte array containing the data.
     */
    private static boolean download(final String[] licenseKeys, long lastModified, 
            String pathToTempFile) throws AutoUpdateException {
        //Declare resources so that they can be released in finally block.
        HttpURLConnection client = null;
        FileOutputStream outputStream = null;
        InputStream inputStream = null;
        try {
            // Open the connection to download the latest data file.
            client = (HttpsURLConnection) fullUrl(licenseKeys).openConnection();

            // Check if a date has been supplied and send it
            if (lastModified != -1) {
                final Date modifiedDate = new Date(lastModified);
                final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
                dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
                client.setRequestProperty("Last-Modified", dateFormat.format(modifiedDate));
            }

            // If data is available then see if it's a new data file.
            if (client.getResponseCode() == HttpsURLConnection.HTTP_OK) {
                //Allocate resources for the download.
                inputStream = client.getInputStream();
                outputStream = new FileOutputStream(pathToTempFile);
                byte[] buffer = new byte[4096];
                int bytesRead = -1;
                
                //Download.
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                
                //Release resources.
                outputStream.close();
                inputStream.close();
                buffer = null;
                
                // now validate with md5 hash
                if (validateMD5(client, pathToTempFile)) {
                    return true;
                } else {
                    throw new AutoUpdateException("Device data update does not match hash values.");
                }
            } else {
                //Server response was not 200. Data download can not commence.
                StringBuilder message = new StringBuilder();
                message.append("Could not commence data file download. ");
                if(client.getResponseCode() == 429) {
                    message.append("Server response: 429 - too many download attempts. ");
                } else if (client.getResponseCode() == 304) {
                    message.append("Server response: 304 - not modified. ");
                    message.append("You already have the latest data.");
                } else if(client.getResponseCode() == 403) {
                    message.append("Server response: 403 - forbidden. ");
                    message.append("Your key is blacklisted. Please contact ");
                    message.append("51Degrees support as soon as possible.");
                } else {
                    message.append("Server response: ");
                    message.append(client.getResponseCode());
                }
                throw new AutoUpdateException(message.toString());
            }
        } catch (IOException ex) {
            throw new AutoUpdateException("Device data download failed: " + ex.getMessage());
        } finally {
            //Release resources.
            if (client != null) {
                client.disconnect();
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException ex) {
                    Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    Logger.getLogger(AutoUpdate.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }

    /**
     * Reads a source GZip file and writes the uncompressed data to destination 
     * file.
     * @param sourcePath path to GZip file to load from.
     * @param destinationPath path to file to write the uncompressed data to.
     * @throws IOException
     * @throws DataFormatException 
     */
    private static void decompressData(String sourcePath, String destinationPath) 
            throws IOException, DataFormatException {
        //Allocate resources.
        FileInputStream fis = new FileInputStream(sourcePath);
        FileOutputStream fos = new FileOutputStream(destinationPath);
        GZIPInputStream gzis = new GZIPInputStream(fis);
        byte[] buffer = new byte[1024];
        int len = 0;
        
        //Extract compressed content.
        while ((len = gzis.read(buffer)) > 0) {
            fos.write(buffer, 0, len);
        }
        
        //Release resources.
        fos.close();
        fis.close();
        gzis.close();
        buffer = null;
    }
    
    /**
     * Method initialises path to the two temporary files used during the auto 
     * update process. Depending on the access method used, the data files can 
     * be set by the user in which case this method will do nothing. If the user 
     * does not set the paths, then a path  will be derived from the path of the 
     * original data file.
     * 
     * The original data file does not have to exist, but the directory provided 
     * must exist and the path should not be a directory.
     * 
     * @param originalFile string path to the master (original) data file.
     * @throws AutoUpdateException if directory is provided instead of file.
     */
    private static void initTempFiles(String originalFile) throws AutoUpdateException {
        //Derive compressed data file path from original data fiel path.
        if (compressedTempFile.isEmpty()) {
            File dataFile = new File(originalFile);
            if (!dataFile.isDirectory()) {
                StringBuilder sb = new StringBuilder();
                sb.append(dataFile.getAbsolutePath());
                sb.append(".");
                sb.append(UUID.randomUUID());
                sb.append(".tmp");
                compressedTempFile = sb.toString();
            } else {
                throw new AutoUpdateException("File path can not be a directory.");
            }
        }
        if (uncompressedTempFile.isEmpty()) {
            File dataFile = new File(originalFile);
            if (!dataFile.isDirectory()) {
                StringBuilder sb = new StringBuilder();
                sb.append(dataFile.getAbsolutePath());
                sb.append(".new");
                uncompressedTempFile = sb.toString();
            } else {
                throw new AutoUpdateException("File path can not be a directory.");
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy