com.vmware.thinapp.common.util.AfUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.thinapp.common.util.AfUtil.java

Source

/* ***********************************************************************
 * VMware ThinApp Factory
 * Copyright (c) 2009-2013 VMware, Inc. All Rights Reserved.
 *
 * 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.
 * ***********************************************************************/

package com.vmware.thinapp.common.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;

import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.InputSupplier;

/**
 * A catch-all class for various utility functions used in AppFactory.
 */
public class AfUtil {
    /** An HTTP header that stores attachment metadata of the resource. */
    public static final String CONTENT_DISPOSITION = "Content-Disposition";

    /** An HTTP header that defines the type of content in the message body */
    public static final String CONTENT_TYPE = "Content-Type";

    /** An HTTP header that defines the length of the message body */
    public static final String CONTENT_LENGTH = "Content-Length";

    /** A regex pattern to extract file name from the Content-Disposition header. */
    public static final Pattern FILENAME_PATTERN = Pattern.compile(".*filename=.+");

    /** A-Z alphabet used for generating random data */
    private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final int MAX_ICON_SIZE_BYTES = 10 * 1024 * 1024; // 10MB

    private static final Random RANDOM = new Random();

    private static final Logger log = LoggerFactory.getLogger(AfUtil.class);

    private AfUtil() {
        // Do nothing, just hide it.
    }

    /**
     * Search a binary file for a given string, and at the location where it is
     * found, overwrite with the new string. This assumes there is enough
     * 'buffer' space in the file to accommodate the new string without going
     * past the EOF or overwriting any critical data.
     *
     * @param file File to change.
     * @param oldString Text to replace.
     * @param newString New text.
     * @param encoding Character encoding.
     * @return
     * @throws IOException
     */
    public static long binaryReplace(File file, String oldString, String newString, String encoding)
            throws IOException {
        boolean success = false;

        /* Convert old string into encoded bytes */
        byte[] oldBytes = oldString.getBytes(encoding);

        /* Read the file */
        byte[] fileBytes = FileCopyUtils.copyToByteArray(file);

        /* Search for the old string */
        int pos = indexOf(fileBytes, oldBytes);
        if (pos >= 0) {
            /* Replace file bytes with new string */
            byte[] newBytes = newString.getBytes(encoding);
            System.arraycopy(newBytes, 0, fileBytes, pos, newBytes.length);

            /* Rewrite the file */
            FileCopyUtils.copy(fileBytes, file);
            success = true;
        }

        return (success ? fileBytes.length : -1);
    }

    /**
     * Pluralize a noun.
     *
     * @param noun Word like "fish" or "broom"
     * @param count How many?
     * @return Word like "fishes" or "brooms" if count > 1.
     */
    public static String plural(String noun, int count) {
        return (count == 1 ? noun : noun.endsWith("s") ? noun + "es" : noun + "s");
    }

    /**
     * Convert into lowercase with uppercase initial.
     *
     * @param string Word like "hello".
     * @return Word like "Hello"
     */
    public static String toInitialCase(String string) {
        if (string.isEmpty()) {
            return string;
        }

        StringBuffer lower = new StringBuffer(string.toLowerCase());
        lower.setCharAt(0, Character.toUpperCase(lower.charAt(0)));
        return lower.toString();
    }

    /**
     * Return true if any of the given strings are null or empty.
     *
     * @param strings
     * @return True if any string is null or empty.
     */
    public static boolean anyEmpty(String... strings) {
        /* strings is null -> true */
        if (strings == null) {
            return true;
        }

        for (String string : strings) {
            if (!StringUtils.hasLength(string)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Create the directory with the given name, including any necessary but
     * nonexistent parent directories.
     *
     * @param dir a directory name with or without path.
     * @return true if and only if the directory was created, along with all
     *    necessary parent directories; false otherwise.
     */
    public static boolean mkdirsIfNotExist(String dir) {
        if (anyEmpty(dir)) {
            throw new IllegalArgumentException("Invalid dir -> " + dir);
        }
        final File file = new File(dir);
        if (file.isDirectory()) {
            return false;
        }
        return file.mkdirs();
    }

    /**
     * Encode a URL path and optional query string according to RFC 2396.
     *
     * @param path Path to resource
     * @param query Query string
     * @return
     */
    public static String escapeUrlPath(String path, String query) {
        try {
            URI uri;
            uri = new URI("http", "authority", path, query, null);
            return uri.getRawPath() + "?" + uri.getRawQuery();
        } catch (URISyntaxException e) {
            /* This should not happen */
            return null;
        }
    }

    /**
     * Added prefix and postfix to a given input string if missing.
     *
     * @param prefix a prefix to append.
     * @param input an input string.
     * @param postfix a postfix to append.
     * @return a new string with prefix and postfix. If the input is empty or null,
     *    it then return just prefix and postfix.
     */
    public static String appendIfNotExist(String prefix, String input, String postfix) {
        if (anyEmpty(input)) {
            return String.valueOf(prefix + postfix);
        }
        final StringBuilder output = new StringBuilder();
        if (!anyEmpty(prefix) && !input.startsWith(prefix)) {
            output.append(prefix);
        }
        output.append(input);
        if (!anyEmpty(postfix) && !input.endsWith(postfix)) {
            output.append(postfix);
        }
        return output.toString();
    }

    /**
     * Convert to a java.net.URL from string url.
     *
     * @param urlStr a string url
     * @return an instance of URL if the url conversion succeeded; return null otherwise.
     */
    public static final URL toURL(String urlStr) {
        try {
            return new URL(urlStr);
        } catch (MalformedURLException ex) {
            return null;
        }
    }

    /**
     * Parse ISO date format date string to java.util.Date.
     *
     * @param dateStr a date string in yyyy-MM-dd format
     * @return an instance of java.util.Date if parsing was successful; otherwise, return null.
     */
    public static Date parseIsoDate(String dateStr) {
        String pattern = DateFormatUtils.ISO_DATE_FORMAT.getPattern();
        Date date = null;

        try {
            date = DateUtils.parseDate(dateStr, new String[] { pattern });
        } catch (ParseException e) {
            /* Eat the exception */
        }
        return date;
    }

    /**
     * Create a random garbage data value for testing.
     * @return A string of 5 chars with random A-Z letters in it.
     */
    public static final String randomString() {
        StringBuilder s = new StringBuilder("     ");
        for (int i = 0; i < s.length(); i++) {
            s.setCharAt(i, ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length())));
        }

        return s.toString();
    }

    /**
     * Search data for a given pattern. This uses the Knuth-Morris-Pratt (KMP)
     * algorithm.
     */
    private static int indexOf(byte[] data, byte[] pattern) {
        int[] failure = computeFailure(pattern);
        int j = 0;

        if (data.length == 0) {
            return -1;
        }

        for (int i = 0; i < data.length; i++) {
            while (j > 0 && pattern[j] != data[i]) {
                j = failure[j - 1];
            }

            if (pattern[j] == data[i]) {
                j++;
            }

            if (j == pattern.length) {
                return i - pattern.length + 1;
            }
        }

        return -1;
    }

    /**
     * Compute the KMP failure function for a given byte pattern.
     */
    private static int[] computeFailure(byte[] pattern) {
        int[] failure = new int[pattern.length];
        int j = 0;

        for (int i = 1; i < pattern.length; i++) {
            while (j > 0 && pattern[j] != pattern[i]) {
                j = failure[j - 1];
            }
            if (pattern[j] == pattern[i]) {
                j++;
            }
            failure[i] = j;
        }

        return failure;
    }

    /**
     * Return true if the given string can be parsed as a URI and
     * begins with a URI scheme (e.g. "http://something"), false otherwise.
     *
     * @param uriString
     * @throws NullPointerException If uriString is null.
     * @return
     */
    public static boolean isAbsoluteUri(String uriString) {
        try {
            URI uri = new URI(uriString);
            return uri.getScheme() != null;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

    /**
     * Checks whether the given URL string begins with a protocol (http://,
     * ftp://, etc.)  If it does, the string is returned unchanged.  If it does
     * not, full URL is returned and is constructed as parentUrl "/" url.
     *
     * @param url input URL string in absolute or relative form
     * @param parentUrl base URL to use if the given URL is in relative form
     * @return an absolute URL
     */
    public static URI relToAbs(String url, URI parentUrl) throws URISyntaxException {
        if (!StringUtils.hasLength(url)) {
            throw new URISyntaxException(url, "The input url was empty!");
        }
        URI parent2 = new URI(parentUrl.getScheme(), parentUrl.getUserInfo(), parentUrl.getAuthority(),
                parentUrl.getPort(), parentUrl.getPath() + "/", // Parent URL path must end with "/" for
                // this to work. resolve() removes any
                // duplicates.
                parentUrl.getQuery(), parentUrl.getFragment());

        return parent2.resolve(url.trim());
    }

    /**
     * Given a URI, return the last uri token.
     *
     * @param uri Last token
     * @return
     */
    public static String extractLastURIToken(URI uri) {
        if (uri == null || !StringUtils.hasLength(uri.getPath())) {
            return null;
        }
        String tokens[] = uri.getPath().split("/");
        return tokens[tokens.length - 1];
    }

    /**
     * Given a URI, return a new URI with the query, fragment, and top level of
     * the path removed.
     *
     * @param uri the input URI
     * @return the base URI
     */
    public static URI parentUri(URI uri) throws URISyntaxException {
        if (uri == null) {
            return null;
        }
        String protocol = uri.getScheme();
        String host = uri.getHost();

        // Process the port number, if any
        int port = uri.getPort();

        // Process the path, if any
        String path = uri.getPath();
        if (!StringUtils.hasLength(path)) {
            path = "";
        } else {
            if (path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }
            if (!path.contains("/")) {
                path = "";
            } else {
                int lastSlash = path.lastIndexOf('/');
                path = path.substring(0, lastSlash);
            }
        }

        // Build the final URL and return it
        return new URI(protocol, null, host, port, path, null, null);
    }

    /**
     * Convert enumeration values into string values.
     *
     * @param values
     * @return
     */
    public static String[] toNames(Enum<?>... values) {
        List<String> names = new ArrayList<String>();

        for (Enum<?> e : values) {
            names.add(e.name());
        }

        return names.toArray(new String[names.size()]);
    }

    /**
     * Convert a string url into a java.net.URI instance.
     *
     * @param url a url string.
     * @return an instance of URI.
     */
    public static URI toURI(String url) {
        if (url == null) {
            throw new IllegalArgumentException("Null url cannot be converted to a URI");
        }
        try {
            return new URI(url);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Convert to a UNC path.
     *
     * @param server a file share server hostname or ip.
     * @param share a share
     * @param path a path
     * @return a UNC formatted string.
     */
    public static String toUNC(String server, String share, String path) {
        if (StringUtils.hasLength(path)) {
            return String.format("//%s/%s/%s", server, share, path).replace("/", "\\");
        }
        return String.format("//%s/%s", server, share).replace("/", "\\");
    }

    /**
     * Attempt to extract a filename from the HTTP headers when accessing the
     * given URL.
     *
     * @param url URL to access
     * @return filename extracted from the Content-Disposition HTTP header, null
     *         if extraction fails.
     */
    public static String getFilenameFromUrl(URL url) {
        String filename = null;

        if (url == null) {
            return null;
        }

        try {
            URLConnection connection = url.openConnection();
            connection.connect();

            // Pull out the Content-Disposition header if there is one
            String contentDisp = connection.getHeaderField(AfUtil.CONTENT_DISPOSITION);

            // Attempt to close the associated stream as we don't need it
            Closeables.closeQuietly(connection.getInputStream());

            // Attempt to extract the filename from the HTTP header
            filename = AfUtil.getFilenameFromContentDisposition(contentDisp);
        } catch (IOException ex) {
            // Unable to make the HTTP request to get the filename from the
            // message headers.
            // Do nothing, null will be returned.
        }
        return filename;
    }

    /**
     * Attempt to extract the filename from the HTTP response header.  If it is
     * not possible, attempt to pull it out of the given URI.  If that is not
     * possible, default to the given default filename.
     *
     * @param response HTTP response to process.
     * @param uri URI to process if HTTP response processing fails.
     * @param defaultFilename default filename to use if
     * @return the filename given the result of processing the given HTTP
     *         response, URI, and default filename.
     */
    public static String getFilenameFromHttpResponse(ClientHttpResponse response, URI uri, String defaultFilename) {
        String filename = AfUtil.getFilenameFromResponseHeader(response);

        // If filename not found in the HTTP response header, try to get it from
        // the given URI.
        if (filename == null) {
            filename = AfUtil.getFilenameFromURI(uri);
            // If it's still not available, then use the given default.
            if (filename == null) {
                filename = defaultFilename;
            }
        }

        return filename;
    }

    /**
     * Extract filename for the HTTP response header.
     *
     * @param response a ClientHttpResponse.
     * @return a filename if found or null.
     */
    public static final String getFilenameFromResponseHeader(ClientHttpResponse response) {
        String contentDisposition = response.getHeaders().getFirst(AfUtil.CONTENT_DISPOSITION);
        if (StringUtils.hasLength(contentDisposition)) {
            return AfUtil.getFilenameFromContentDisposition(contentDisposition);
        }
        return null;
    }

    /**
     * Attempt to extract the filename from the given Content-Disposition header
     * string.  Example of this header string:
     *
     * "attachment; filename=name_of_file.txt"
     *
     * @param contentDisposition string from a HTTP Content-Disposition header
     * @return the filename extracted from the given string, null if parsing the
     *         string fails.
     */
    public static final String getFilenameFromContentDisposition(String contentDisposition) {
        if (contentDisposition == null) {
            return null;
        }
        final Matcher matcher = AfUtil.FILENAME_PATTERN.matcher(contentDisposition);
        if (matcher.matches()) {
            String[] temp = contentDisposition.split("filename=");
            if (temp != null && temp.length > 1) {
                String filename = temp[1];
                /** filename could be in double quotes. */
                filename = StringUtils.deleteAny(filename, "\"");
                return filename;
            }
        }
        return null;
    }

    /**
     * Parse a filename from the given URI.
     *
     * @param uri a uri
     * @return a filename if the uri ends with just a resource, null otherwise
     */
    public static final String getFilenameFromURI(URI uri) {
        String path = uri.getPath();
        int lastSlash = path.lastIndexOf('/');

        // Is the last slash at the end?
        if (lastSlash == path.length() - 1) {
            return null;
        }
        return path.substring(lastSlash + 1);
    }

    /**
     * Simple container for an icon's binary data and content type.
     */
    public static class IconInfo {
        public byte[] data;
        public String contentType;

        public IconInfo(byte[] data, String contentType) {
            this.data = data;
            this.contentType = contentType;
        }
    }

    /**
     * Attempt to download the icon specified by the given URL.  If the resource at the URL
     * has a content type of image/*, the binary data for this resource will be downloaded.
     *
     * @param iconUrlStr URL of the image resource to access
     * @return the binary data and content type of the image resource at the given URL, null
     * if the URL is invalid, the resource does not have a content type starting with image/, or
     * on some other failure.
     */
    public static final @Nullable IconInfo getIconInfo(String iconUrlStr) {
        if (!StringUtils.hasLength(iconUrlStr)) {
            log.debug("No icon url exists.");
            return null;
        }
        URL iconUrl = null;
        try {
            // Need to encode any invalid characters before creating the URL object
            iconUrl = new URL(UriUtils.encodeHttpUrl(iconUrlStr, "UTF-8"));
        } catch (MalformedURLException ex) {
            log.debug("Malformed icon URL string: {}", iconUrlStr, ex);
            return null;
        } catch (UnsupportedEncodingException ex) {
            log.debug("Unable to encode icon URL string: {}", iconUrlStr, ex);
            return null;
        }

        // Open a connection with the given URL
        final URLConnection conn;
        final InputStream inputStream;
        try {
            conn = iconUrl.openConnection();
            inputStream = conn.getInputStream();
        } catch (IOException ex) {
            log.debug("Unable to open connection to URL: {}", iconUrlStr, ex);
            return null;
        }

        String contentType = conn.getContentType();
        int sizeBytes = conn.getContentLength();

        try {
            // Make sure the resource has an appropriate content type
            if (!conn.getContentType().startsWith("image/")) {
                log.debug("Resource at URL {} does not have a content type of image/*.", iconUrlStr);
                return null;
                // Make sure the resource is not too large
            } else if (sizeBytes > MAX_ICON_SIZE_BYTES) {
                log.debug("Image resource at URL {} is too large: {}", iconUrlStr, sizeBytes);
                return null;
            } else {
                // Convert the resource to a byte array
                byte[] iconBytes = ByteStreams.toByteArray(new InputSupplier<InputStream>() {
                    @Override
                    public InputStream getInput() throws IOException {
                        return inputStream;
                    }
                });
                return new IconInfo(iconBytes, contentType);
            }
        } catch (IOException e) {
            log.debug("Error reading resource data.", e);
            return null;
        } finally {
            Closeables.closeQuietly(inputStream);
        }
    }

    /**
     * Chunk digits or non-digits from a string starting at an index.
     *
     * @param s
     * @param length
     * @param index
     * @return
     */
    public static String getDigitOrNonDigitChunk(String s, int length, int index) {
        StringBuilder sb = new StringBuilder();
        char c = s.charAt(index);
        sb.append(c);
        index++;
        boolean digitOrNot = Character.isDigit(c);
        while (index < length) {
            c = s.charAt(index);
            if (digitOrNot != Character.isDigit(c)) {
                break;
            }
            sb.append(c);
            index++;
        }
        return sb.toString();
    }

    /**
     * Compare two string by chuning digits and non-digits separately and compare between the two.
     * This way, the version strings (Ex: 10.2c.4.24546 sp1) can be properly compared.
     *
     * @param str1
     * @param str2
     * @return
     */
    public static int alnumCompare(String str1, String str2) {
        // Ensure null cases are handled for both s1 and s2.
        if (str1 == str2) {
            return 0;
        } else if (str1 == null) {
            return -1;
        } else if (str2 == null) {
            return 1;
        }
        int s1Length = str1.length();
        int s2Length = str2.length();

        for (int s1Index = 0, s2Index = 0; s1Index < s1Length && s2Index < s2Length;) {
            String thisStr = getDigitOrNonDigitChunk(str1, s1Length, s1Index);
            s1Index += thisStr.length();

            String thatStr = getDigitOrNonDigitChunk(str2, s2Length, s2Index);
            s2Index += thatStr.length();

            // If both strs contain numeric characters, sort them numerically
            int result = 0;
            if (Character.isDigit(thisStr.charAt(0)) && Character.isDigit(thatStr.charAt(0))) {
                // Simple str comparison by length.
                int thisStrLength = thisStr.length();
                result = thisStrLength - thatStr.length();
                // If equal, the first different number counts
                if (result == 0) {
                    for (int i = 0; i < thisStrLength; i++) {
                        result = thisStr.charAt(i) - thatStr.charAt(i);
                        if (result != 0) {
                            return result;
                        }
                    }
                }
            } else {
                result = thisStr.compareToIgnoreCase(thatStr);
            }

            if (result != 0) {
                return result;
            }
        }
        return s1Length - s2Length;
    }
}