com.joyent.manta.util.MantaUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.joyent.manta.util.MantaUtils.java

Source

/*
 * Copyright (c) 2013-2017, Joyent, Inc. All rights reserved.
 *
 * 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/.
 */
package com.joyent.manta.util;

import io.mikael.urlbuilder.util.Decoder;
import io.mikael.urlbuilder.util.Encoder;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ContextedException;
import org.apache.commons.lang3.exception.ExceptionContext;
import org.apache.http.client.utils.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.joyent.manta.client.MantaClient.SEPARATOR;

/**
 * Manta utilities.
 *
 * @author Yunong Xiao
 * @author <a href="https://github.com/dekobon">Elijah Zupancic</a>
 */
public final class MantaUtils {
    /**
     * Logger instance.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(MantaUtils.class);

    /**
     * Shared url encoder instance.
     */
    private static final Encoder UTF8_URL_ENCODER = new Encoder(StandardCharsets.UTF_8);

    /**
     * Shared url decoder instance.
     */

    private static final Decoder UTF8_URL_DECODER = new Decoder(StandardCharsets.UTF_8);

    /**
     * Default no-args constructor.
     */
    private MantaUtils() {
    }

    /**
     * Checks to see if a {@link StringBuilder} ends with a given character.
     *
     * @param builder StringBuilder to check
     * @param match character to match
     * @return true if last character in StringBuilder matches
     */
    public static boolean endsWith(final StringBuilder builder, final char match) {
        Validate.notNull(builder, "StringBuilder object must not be null");

        if (builder.length() == 0) {
            return false;
        }

        final char last = builder.subSequence(builder.length() - 1, builder.length()).charAt(0);
        return last == match;
    }

    /**
     * Converts an object's toString value to a String. If an empty string, the return null.
     *
     * @param value object to parse .toString() value from
     * @return null if toString() returns empty or if the passed in value is null, otherwise toString() value
     */
    public static String toStringEmptyToNull(final Object value) {
        if (value == null) {
            return null;
        }

        String stringValue = value.toString();

        if (stringValue.isEmpty()) {
            return null;
        }

        return stringValue;
    }

    /**
     * Parses an arbitrary object for an integer. If it can't be found, return null.
     *
     * @param value Object to parse for an integer
     * @return if parsing fails, return null
     */
    public static Integer parseIntegerOrNull(final Object value) {
        if (value == null) {
            return null;
        }

        if (value instanceof Number) {
            return ((Number) value).intValue();
        }

        String string = toStringEmptyToNull(value);
        if (string == null) {
            return null;
        }

        Integer parsed;

        try {
            parsed = Integer.parseInt(string);
        } catch (Exception e) {
            String msg = "Error parsing value as integer. Value: %s";
            LOGGER.warn(String.format(msg, value), e);
            parsed = null;
        }

        return parsed;
    }

    /**
     * Parses an arbitrary object for a boolean. If it can't be found, return null.
     *
     * @param value Object to parse for an boolean
     * @return if parsing fails, return null
     */
    public static Boolean parseBooleanOrNull(final Object value) {
        if (value == null) {
            return null;
        }

        if (value instanceof Boolean) {
            return (Boolean) value;
        }

        String string = toStringEmptyToNull(value);
        if (string == null) {
            return null;
        }

        if (string.equals("1")) {
            return true;
        } else if (string.equals("0")) {
            return false;
        }

        Boolean parsed;

        try {
            parsed = BooleanUtils.toBoolean(string);
        } catch (Exception e) {
            String msg = "Error parsing value as boolean. Value: %s";
            LOGGER.warn(String.format(msg, value), e);
            parsed = null;
        }

        return parsed;
    }

    /**
     * Parses an arbitrary object for an enum represented as a String. If the
     * string is empty or null we return null. If the String doesn't match any
     * valid enum values, then we return null
     *
     * @param value object to parse string value from
     * @param enumClass enum class to parse
     * @param <T> enum type to parse
     * @return value of enum or null on invalid input
     */
    public static <T extends Enum<T>> T parseEnumOrNull(final Object value, final Class<T> enumClass) {
        if (value == null) {
            return null;
        }

        if (value.getClass() == enumClass) {
            @SuppressWarnings("unchecked")
            T cast = (T) value;
            return cast;
        }

        String string = toStringEmptyToNull(value);
        if (string == null) {
            return null;
        }

        T parsed;

        try {
            parsed = Enum.valueOf(enumClass, string);
        } catch (RuntimeException e) {
            String msg = "Error parsing value as enum. Value: %s";
            LOGGER.warn(String.format(msg, value), e);
            parsed = null;
        }

        return parsed;
    }

    /**
     * Format the manta path for inclusion in a URI path according to
     * RFC3986.  Note that the encoding rules vary for each part of
     * the url
     *
     * @param path the raw path string.
     * @return the URI formatted string with the exception of '/' which is special in manta.
     */
    public static String formatPath(final String path) {
        return UTF8_URL_ENCODER.encodePath(path);
    }

    /**
     * Decodes a percent encoded manta path.
     *
     * @param encodedPath The percent encoded path
     * @return The percent decoded path
     */
    public static String decodePath(final String encodedPath) {
        return UTF8_URL_DECODER.decodePath(encodedPath);
    }

    /**
     * Parses username and subuser from an account name.
     * @param account account name to parse
     * @return an array of containing first the username, then optionally
     *         the subuser (if it exists)
     */
    public static String[] parseAccount(final String account) {
        Validate.notNull(account, "Account must not be null");

        final int slashPos = account.indexOf("/");

        if (account.isEmpty()) {
            throw new IllegalArgumentException("Username can't be empty");
        } else if (slashPos == -1) {
            return new String[] { account };
        } else if (slashPos == 0) {
            throw new IllegalArgumentException("Username can't begin with /");
        } else if (account.charAt(account.length() - 1) == '/') {
            throw new IllegalArgumentException("Username can't end with /");
        }

        final String username = account.substring(0, slashPos);
        final String subuser = account.substring(slashPos + 1);

        return new String[] { username, subuser };
    }

    /**
     * Serializes a specified value to a {@link String}.
     * @param value the value to be serialized
     * @return a serialized value as a {@link String}
     */
    public static String asString(final Object value) {
        if (value == null) {
            return null;
        } else if (value instanceof Enum<?>) {
            Enum<?> enumValue = (Enum<?>) value;

            try {
                /* In this case, we actually want the subclass of the enum type
                 * because we are trying to read a property from it via
                 * reflection. */
                @SuppressWarnings("GetClassOnEnum")
                Field field = enumValue.getClass().getField(enumValue.name());
                Validate.notNull(field, "A non-null field should always be returned. "
                        + "Enum constant missing @Value or @NullValue annotation: %s", enumValue);
            } catch (NoSuchFieldException e) {
                String msg = String.format("Could not find name field for enum: %s", value);
                LOGGER.warn(msg, e);
                return null;
            }
        } else if (value instanceof Iterable<?>) {
            StringBuilder sb = new StringBuilder();

            Iterator<?> itr = ((Iterable<?>) value).iterator();
            while (itr.hasNext()) {
                Object next = itr.next();

                if (next != null) {
                    sb.append(next.toString());
                }

                if (itr.hasNext()) {
                    sb.append(",");
                }
            }

            return sb.toString();
        } else if (value.getClass().isArray()) {
            Object[] array = (Object[]) value;

            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < array.length; i++) {
                Object next = array[i];

                if (next != null) {
                    sb.append(next.toString());
                }

                if (i < array.length - 1) {
                    sb.append(", ");
                }
            }

            return sb.toString();
        }

        return value.toString();
    }

    /**
     * Converts a naive CSV string to a collection.
     *
     * @param line CSV string
     * @return collection containing each value between each comma
     */
    public static Collection<String> fromCsv(final String line) {
        Validate.notNull(line, "Line must not be null");

        final List<String> list = new ArrayList<>();

        if (line.contains(",")) {
            String[] parts = line.split(",\\s*");
            Collections.addAll(list, parts);
        } else {
            list.add(line);
        }

        return list;
    }

    /**
     * Extracts the last file or directory from the path provided.
     *
     * @param path URL or Unix-style file path
     * @return the last file or directory in path
     */
    public static String lastItemInPath(final String path) {
        Validate.notEmpty(path, "Path must not be null nor empty");

        final Path asNioPath = Paths.get(path);
        final int count = asNioPath.getNameCount();

        if (count < 1) {
            throw new IllegalArgumentException("Path doesn't have a single element to parse");
        }

        final Path lastPart = asNioPath.getName(count - 1);
        return lastPart.toString();
    }

    /**
     * Compute the intermediate directories between "/" and {@code rawPath}.
     *
     * @param rawPath The fully qualified path of the Manta directory.
     * @return All non-root directories leading to and including rawPath
     */
    public static String[] prefixPaths(final String rawPath) {
        final String[] parts = StringUtils.split(rawPath, SEPARATOR.charAt(0));
        final Iterator<Path> itr = Paths.get("", parts).iterator();
        final StringBuilder sb = new StringBuilder(SEPARATOR);
        final String[] paths = new String[parts.length];

        for (int i = 0; itr.hasNext(); i++) {
            final String part = itr.next().toString();
            sb.append(part);

            paths[i] = sb.toString();

            if (itr.hasNext()) {
                sb.append(SEPARATOR);
            }
        }

        return paths;
    }

    /**
     * Returns intermediate directories between "/" and {@code rawPath}, not including
     * the user's home folder and or system directories.
     *
     * @param rawPath The fully qualified path of the Manta directory.
     * @return All non-root and non-system directories including rawPath
     */
    public static String[] writeablePrefixPaths(final String rawPath) {
        final String[] paths = prefixPaths(rawPath);
        return Arrays.copyOfRange(prefixPaths(rawPath), 2, paths.length);
    }

    /**
     * Converts a map of string to object values to a pure string map.
     *
     * @param map map to convert
     * @return a string map
     */
    public static Map<String, String> asStringMap(final Map<String, ?> map) {
        Validate.notNull(map, "Map must not be null");

        if (map.isEmpty()) {
            return Collections.emptyMap();
        }

        final Map<String, String> stringMap = new LinkedHashMap<>(map.size());

        // Silly Java generics won't covert wildcard to simple generic
        @SuppressWarnings("unchecked")
        final Map<String, Object> objectMap = (Map<String, Object>) map;
        final Set<Map.Entry<String, Object>> entrySet = objectMap.entrySet();

        for (Map.Entry<String, Object> next : entrySet) {
            final Object obj = next.getValue();
            final String value;

            if (obj == null || obj instanceof String) {
                value = (String) obj;
            } else if (obj instanceof InetAddress) {
                value = ((InetAddress) obj).getHostAddress();
            } else if (obj instanceof Map) {
                value = csv((Map) obj);
            } else if (obj instanceof Iterable) {
                value = csv((Iterable) obj);
            } else if (obj instanceof CharSequence) {
                value = String.valueOf(obj);
            } else {
                value = obj.toString();
            }

            stringMap.put(next.getKey(), value);
        }

        return Collections.unmodifiableMap(stringMap);
    }

    /**
     * Naively converts a map to a single CSV string. Warning: this doesn't
     * escape.
     *
     * @param map map with objects with implemented toString methods
     * @return CSV string or empty string
     */
    public static String csv(final Map<?, ?> map) {
        Validate.notNull(map, "Map must not be null");

        final StringBuilder builder = new StringBuilder();

        /* We do this contorted type conversion because of Java's generics. */
        @SuppressWarnings("rawtypes")
        final Map noGenericsMap = map;
        @SuppressWarnings({ "rawtypes", "unchecked" })
        final Set<Map.Entry<?, ?>> set = noGenericsMap.entrySet();

        final Iterator<Map.Entry<?, ?>> itr = set.iterator();

        while (itr.hasNext()) {
            Map.Entry<?, ?> entry = itr.next();

            if (entry == null || entry.getKey() == null) {
                continue;
            }

            builder.append(entry.getKey().toString()).append(": ").append(String.valueOf(entry.getValue()));

            if (itr.hasNext()) {
                builder.append(", ");
            }
        }

        return builder.toString();
    }

    /**
     * Naively converts a collection of objects to a single CSV string.
     * Warning: this doesn't escape.
     * @param stringable collection of objects with implemented toString methods
     * @return CSV string or empty string
     */
    public static String csv(final Iterable<?> stringable) {
        if (stringable == null) {
            return "";
        }

        final StringBuilder builder = new StringBuilder();

        Iterator<?> itr = stringable.iterator();

        while (itr.hasNext()) {
            final Object o = itr.next();

            if (o == null) {
                continue;
            }

            final String value;

            if (o instanceof InetAddress) {
                value = ((InetAddress) o).getHostAddress();
            } else if (o instanceof Map) {
                StringBuilder sb = new StringBuilder();

                @SuppressWarnings({ "unchecked", "rawtypes" })
                final Map map = (Map) o;
                @SuppressWarnings("unchecked")
                final Iterator<Map.Entry<?, ?>> mapItr = (Iterator<Map.Entry<?, ?>>) map.entrySet().iterator();

                while (mapItr.hasNext()) {
                    Map.Entry<?, ?> entry = mapItr.next();
                    sb.append("[").append(String.valueOf(entry.getKey())).append("=").append(entry.getValue())
                            .append("]");

                    if (mapItr.hasNext()) {
                        sb.append(" ");
                    }
                }

                value = sb.toString();
            } else {
                value = o.toString();
            }

            // Strip any commas out of the string
            builder.append(StringUtils.replaceChars(value, ',', ' '));

            if (itr.hasNext()) {
                builder.append(", ");
            }
        }

        return builder.toString();
    }

    /**
     * Naively converts a CSV string into an array.
     *
     * @param line non-null String containing comma delimiters
     * @return an array of Strings for each token between a comma
     */
    public static String[] csv2array(final String line) {
        Validate.notNull(line, "Line must not be null");

        if (line.contains(",")) {
            return line.split(",\\s*");
        } else {
            return new String[] { line };
        }
    }

    /**
     * Adds the passed exceptions as properties to a {@link ContextedException}
     * instance.
     *
     * @param contexted exception to attach exceptions to
     * @param exceptions exceptions to attach
     */
    public static void attachExceptionsToContext(final ExceptionContext contexted,
            final Iterable<? extends Exception> exceptions) {
        int count = 1;
        for (Exception e : exceptions) {
            final String label = String.format("exception_%d", count++);
            contexted.setContextValue(label, e);
        }
    }

    /**
     * Parses a HTTP Date header value.
     *
     * @param date header to parse
     * @return instance of Date based on input
     */
    public static Date parseHttpDate(final String date) {
        return DateUtils.parseDate(date);
    }

    /**
     * Byte array as represented as a string of hex digits.
     * @param bytes byte array to convert
     * @return null on null input or string of hex digits
     */
    public static String byteArrayAsHexString(final byte[] bytes) {
        if (bytes == null) {
            return null;
        }

        return Hex.encodeHexString(bytes);
    }

    /**
     * Creates a new unmodifiable {@link HashMap} with the passed parameters.
     *
     * @param key1 First key value pair's key
     * @param val1 First key value pair's valueS
     * @param <K> type of key for the map
     * @param <V> type of value for the map
     * @return new instance populated with the passed values
     */
    public static <K, V> Map<K, V> unmodifiableMap(final K key1, final V val1) {
        Map<K, V> map = Collections.singletonMap(key1, val1);

        return Collections.unmodifiableMap(map);
    }

    /**
     * Creates a new unmodifiable {@link HashMap} with the passed parameters.
     *
     * @param key1 First key value pair's key
     * @param val1 First key value pair's value
     * @param key2 Second key value pair's key
     * @param val2 Second key value pair's value
     * @param <K> type of key for the map
     * @param <V> type of value for the map
     * @return new instance populated with the passed values
     */
    public static <K, V> Map<K, V> unmodifiableMap(final K key1, final V val1, final K key2, final V val2) {
        Map<K, V> map = new LinkedHashMap<>(2);
        map.put(key1, val1);
        map.put(key2, val2);

        return Collections.unmodifiableMap(map);
    }

    /**
     * Creates a new unmodifiable {@link HashMap} with the passed parameters.
     *
     * @param key1 First key value pair's key
     * @param val1 First key value pair's value
     * @param key2 Second key value pair's key
     * @param val2 Second key value pair's value
     * @param key3 Third key value pair's key
     * @param val3 Third key value pair's value
     * @param <K> type of key for the map
     * @param <V> type of value for the map
     * @return new instance populated with the passed values
     */
    public static <K, V> Map<K, V> unmodifiableMap(final K key1, final V val1, final K key2, final V val2,
            final K key3, final V val3) {
        @SuppressWarnings("MagicNumber")
        Map<K, V> map = new LinkedHashMap<>(3);
        map.put(key1, val1);
        map.put(key2, val2);
        map.put(key3, val3);

        return Collections.unmodifiableMap(map);
    }

    /**
     * Creates a new unmodifiable {@link HashMap} with the passed parameters.
     *
     * @param key1 First key value pair's key
     * @param val1 First key value pair's value
     * @param nPairs vararg array of key value pairs to add to the map
     * @param <K> type of key for the map
     * @param <V> type of value for the map
     * @return new instance populated with the passed values
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> unmodifiableMap(final K key1, final V val1, final Object... nPairs) {
        if (nPairs == null) {
            return unmodifiableMap(key1, val1);
        }

        if (nPairs.length % 2 != 0) {
            throw new IllegalArgumentException("Vararg must be provided in even numbers");
        }

        Map<K, V> map = new LinkedHashMap<>(1 + (nPairs.length / 2));
        map.put(key1, val1);

        K key = null;
        V val;

        for (int i = 0; i < nPairs.length; i++) {
            if (i % 2 == 0) {
                key = (K) nPairs[i];
            } else {
                val = (V) nPairs[i];
                map.put(key, val);
            }
        }

        return Collections.unmodifiableMap(map);
    }

    /**
     * Method that converts long values that are over {@link Integer#MAX_VALUE}
     * and under {@link Integer#MIN_VALUE} to their respective integer max and
     * min values.
     *
     * @param value value to convert
     * @return converted value if overflow, otherwise identical value as int
     */
    public static int overflowToIntMax(final long value) {
        if (value > Integer.MAX_VALUE) {
            return Integer.MAX_VALUE;
        }

        if (value < Integer.MIN_VALUE) {
            return Integer.MIN_VALUE;
        }

        return (int) value;
    }
}