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

org.spongepowered.api.util.Coerce Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of SpongeAPI, licensed under the MIT License (MIT).
 *
 * Copyright (c) SpongePowered 
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.spongepowered.api.util;

import com.google.common.collect.Lists;
import com.google.common.primitives.Booleans;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Chars;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.primitives.Shorts;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.math.vector.Vector2i;
import org.spongepowered.math.vector.Vector3i;
import org.spongepowered.math.vector.Vector4i;
import org.spongepowered.math.vector.VectorNi;
import org.spongepowered.math.vector.Vectord;
import org.spongepowered.math.vector.Vectorf;
import org.spongepowered.math.vector.Vectorl;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class for coercing unknown values to specific target types.
 */
public final class Coerce {

    private static final Pattern listPattern = Pattern.compile("^([\\(\\[\\{]?)(.+?)([\\)\\]\\}]?)$");

    private static final String[] listPairings = { "([{", ")]}" };

    private static final Pattern vector2Pattern = Pattern.compile("^\\( *(-?[\\d\\.]{1,10}), *(-?[\\d\\.]{1,10}) *\\)$");

    /**
     * No subclasses for you.
     */
    private Coerce() {}

    /**
     * Coerce the supplied object to a string.
     *
     * @param obj Object to coerce
     * @return Object as a string, empty string if the object is null
     */
    public static String toString(@Nullable Object obj) {
        if (obj == null) {
            return "";
        }

        if (obj.getClass().isArray()) {
            return Coerce.toList(obj).toString();
        }

        return obj.toString();
    }

    /**
     * Gets the given object as a {@link String}.
     *
     * @param obj The object to translate
     * @return The string value, if available
     */
    public static Optional asString(@Nullable Object obj) {
        if (obj instanceof String) {
            return Optional.of((String) obj);
        } else if (obj == null) {
            return Optional.empty();
        } else {
            return Optional.of(obj.toString());
        }
    }

    /**
     * Coerce the supplied object to a list. Accepts lists and all types of 1D
     * arrays. Also (naively) supports lists in Strings in a format like
     * {1,2,3,I,am,a,list}
     *
     * @param obj Object to coerce
     * @return Some kind of List filled with unimaginable horrors
     */
    public static List toList(@Nullable Object obj) {
        if (obj == null) {
            return Collections.emptyList();
        }

        if (obj instanceof List) {
            return (List) obj;
        }

        final Class clazz = obj.getClass();
        if (clazz.isArray()) {
            if (clazz.getComponentType().isPrimitive()) {
                return Coerce.primitiveArrayToList(obj);
            }

            return Arrays.asList((Object[]) obj);
        }

        return Coerce.parseStringToList(obj.toString());
    }

    /**
     * Gets the given object as a {@link List}.
     *
     * @param obj The object to translate
     * @return The list, if available
     */
    public static Optional> asList(@Nullable Object obj) {
        if (obj == null) {
            return Optional.empty();
        }

        if (obj instanceof List) {
            return Optional.>of((List) obj);
        }

        final Class clazz = obj.getClass();
        if (clazz.isArray()) {
            if (clazz.getComponentType().isPrimitive()) {
                return Optional.>of(Coerce.primitiveArrayToList(obj));
            }

            return Optional.>of(Arrays.asList((Object[]) obj));
        }

        return Optional.>of(Coerce.parseStringToList(obj.toString()));
    }

    /**
     * Coerce the specified object to a list containing only objects of type
     * specified by ofClass. Also coerces list values where
     * possible.
     *
     * @param obj Object to coerce
     * @param ofClass Class to coerce to
     * @param  type of list (notional)
     * @return List of coerced values
     */
    @SuppressWarnings("unchecked")
    public static  List toListOf(@Nullable Object obj, Class ofClass) {
        Objects.requireNonNull(ofClass, "ofClass");
        final List filteredList = Lists.newArrayList();

        for (Object o : Coerce.toList(obj)) {
            if (ofClass.isAssignableFrom(o.getClass())) {
                filteredList.add((T) o);
            } else if (ofClass.equals(String.class)) {
                filteredList.add((T) Coerce.toString(o));
            } else if (ofClass.equals(Integer.TYPE) || ofClass.equals(Integer.class)) {
                filteredList.add((T) (Integer) Coerce.toInteger(o));
            } else if (ofClass.equals(Float.TYPE) || ofClass.equals(Float.class)) {
                filteredList.add((T) Float.valueOf((float) Coerce.toDouble(o)));
            } else if (ofClass.equals(Double.TYPE) || ofClass.equals(Double.class)) {
                filteredList.add((T) (Double) Coerce.toDouble(o));
            } else if (ofClass.equals(Boolean.TYPE) || ofClass.equals(Boolean.class)) {
                filteredList.add((T) (Boolean) Coerce.toBoolean(o));
            }
        }

        return filteredList;
    }

    /**
     * Coerce the supplied object to a boolean, matches strings such as "yes" as
     * well as literal boolean values.
     *
     * @param obj Object to coerce
     * @return Object as a boolean, false if the object is null
     */
    public static boolean toBoolean(@Nullable Object obj) {
        if (obj == null) {
            return false;
        }

        return (obj instanceof Boolean) ? (Boolean) obj : obj.toString().trim().matches("^(1|true|yes)$");
    }

    /**
     * Gets the given object as a {@link Boolean}.
     *
     * @param obj The object to translate
     * @return The boolean, if available
     */
    public static Optional asBoolean(@Nullable Object obj) {
        if (obj instanceof Boolean) {
            return Optional.of((Boolean) obj);
        } else if (obj instanceof Byte) {
            return Optional.of((Byte) obj != 0);
        }
        return Optional.empty();
    }

    /**
     * Coerce the supplied object to an integer, parse it if necessary.
     *
     * @param obj Object to coerce
     * @return Object as an integer, 0 if the object is null or
     *         cannot be parsed
     */
    public static int toInteger(@Nullable Object obj) {
        if (obj == null) {
            return 0;
        }

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

        final String strObj = Coerce.sanitiseNumber(obj);
        final Integer iParsed = Ints.tryParse(strObj);
        if (iParsed != null) {
            return iParsed;
        }

        final Double dParsed = Doubles.tryParse(strObj);
        return dParsed != null ? dParsed.intValue() : 0;
    }

    /**
     * Gets the given object as a {@link Integer}.
     *
     * 

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The integer value, if available */ public static Optional asInteger(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).intValue()); } try { return Optional.ofNullable(Integer.valueOf(obj.toString())); } catch (NumberFormatException | NullPointerException e) { // do nothing } final String strObj = Coerce.sanitiseNumber(obj); final Integer iParsed = Ints.tryParse(strObj); if (iParsed == null) { final Double dParsed = Doubles.tryParse(strObj); // try parsing as double now return dParsed == null ? Optional.empty() : Optional.of(dParsed.intValue()); } return Optional.of(iParsed); } /** * Coerce the supplied object to a double-precision floating-point number, * parse it if necessary. * * @param obj Object to coerce * @return Object as a double, 0.0 if the object is null or * cannot be parsed */ public static double toDouble(@Nullable Object obj) { if (obj == null) { return 0.0; } if (obj instanceof Number) { return ((Number) obj).doubleValue(); } final Double parsed = Doubles.tryParse(Coerce.sanitiseNumber(obj)); return parsed != null ? parsed : 0.0; } /** * Gets the given object as a {@link Double}. * *

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The double value, if available */ public static Optional asDouble(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).doubleValue()); } try { return Optional.ofNullable(Double.valueOf(obj.toString())); } catch (NumberFormatException | NullPointerException e) { // do nothing } final String strObj = Coerce.sanitiseNumber(obj); final Double dParsed = Doubles.tryParse(strObj); // try parsing as double now return dParsed == null ? Optional.empty() : Optional.of(dParsed); } /** * Coerce the supplied object to a single-precision floating-point number, * parse it if necessary. * * @param obj Object to coerce * @return Object as a float, 0.0 if the object is null or * cannot be parsed */ public static float toFloat(@Nullable Object obj) { if (obj == null) { return 0.0f; } if (obj instanceof Number) { return ((Number) obj).floatValue(); } final Float parsed = Floats.tryParse(Coerce.sanitiseNumber(obj)); return parsed != null ? parsed : 0.0f; } /** * Gets the given object as a {@link Float}. * *

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The float value, if available */ public static Optional asFloat(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).floatValue()); } try { return Optional.ofNullable(Float.valueOf(obj.toString())); } catch (NumberFormatException | NullPointerException e) { // do nothing } final String strObj = Coerce.sanitiseNumber(obj); final Double dParsed = Doubles.tryParse(strObj); return dParsed == null ? Optional.empty() : Optional.of(dParsed.floatValue()); } /** * Coerce the supplied object to a short number, parse it if necessary. * * @param obj Object to coerce * @return Object as a short, 0 if the object is null or cannot * be parsed */ public static short toShort(@Nullable Object obj) { if (obj == null) { return 0; } if (obj instanceof Number) { return ((Number) obj).shortValue(); } try { return Short.parseShort(Coerce.sanitiseNumber(obj)); } catch (NumberFormatException e) { return 0; } } /** * Gets the given object as a {@link Short}. * *

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The short value, if available */ public static Optional asShort(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).shortValue()); } try { return Optional.ofNullable(Short.parseShort(Coerce.sanitiseNumber(obj))); } catch (NumberFormatException | NullPointerException e) { // do nothing } return Optional.empty(); } /** * Coerce the supplied object to a byte number, parse it if necessary. * * @param obj Object to coerce * @return Object as a byte, 0 if the object is null or cannot * be parsed */ public static byte toByte(@Nullable Object obj) { if (obj == null) { return 0; } if (obj instanceof Number) { return ((Number) obj).byteValue(); } try { return Byte.parseByte(Coerce.sanitiseNumber(obj)); } catch (NumberFormatException e) { return 0; } } /** * Gets the given object as a {@link Byte}. * *

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The byte value, if available */ public static Optional asByte(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).byteValue()); } try { return Optional.ofNullable(Byte.parseByte(Coerce.sanitiseNumber(obj))); } catch (NumberFormatException | NullPointerException e) { // do nothing } return Optional.empty(); } /** * Coerce the supplied object to a long number, parse it if necessary. * * @param obj Object to coerce * @return Object as a long, 0 if the object is null or cannot * be parsed */ public static long toLong(@Nullable Object obj) { if (obj == null) { return 0; } if (obj instanceof Number) { return ((Number) obj).longValue(); } try { return Long.parseLong(Coerce.sanitiseNumber(obj)); } catch (NumberFormatException e) { return 0; } } /** * Gets the given object as a {@link Long}. * *

Note that this does not translate numbers spelled out as strings.

* * @param obj The object to translate * @return The long value, if available */ public static Optional asLong(@Nullable Object obj) { if (obj == null) { // fail fast return Optional.empty(); } if (obj instanceof Number) { return Optional.of(((Number) obj).longValue()); } try { return Optional.ofNullable(Long.parseLong(Coerce.sanitiseNumber(obj))); } catch (NumberFormatException | NullPointerException e) { // do nothing } return Optional.empty(); } /** * Coerce the supplied object to a character, parse it if necessary. * * @param obj Object to coerce * @return Object as a character, '\u0000' if the object is * null or cannot be parsed */ public static char toChar(@Nullable Object obj) { if (obj == null) { return 0; } if (obj instanceof Character) { return (Character) obj; } try { return obj.toString().charAt(0); } catch (Exception e) { // do nothing } return '\u0000'; } /** * Gets the given object as a {@link Character}. * * @param obj The object to translate * @return The character, if available */ public static Optional asChar(@Nullable Object obj) { if (obj == null) { return Optional.empty(); } if (obj instanceof Character) { return Optional.of((Character) obj); } try { return Optional.of(obj.toString().charAt(0)); } catch (Exception e) { // do nothing } return Optional.empty(); } /** * Coerce the specified object to an enum of the supplied type, returns the * first enum constant in the enum if parsing fails. * * @param obj Object to coerce * @param enumClass Enum class to coerce to * @param enum type * @return Coerced enum value */ public static > E toEnum(@Nullable Object obj, Class enumClass) { return Coerce.toEnum(obj, enumClass, enumClass.getEnumConstants()[0]); } /** * Coerce the specified object to an enum of the supplied type, returns the * specified default value if parsing fails. * * @param obj Object to coerce * @param enumClass Enum class to coerce to * @param defaultValue default value to return if coercion fails * @param enum type * @return Coerced enum value */ public static > E toEnum(@Nullable Object obj, Class enumClass, E defaultValue) { Objects.requireNonNull(enumClass, "enumClass"); Objects.requireNonNull(defaultValue, "defaultValue"); if (obj == null) { return defaultValue; } if (enumClass.isAssignableFrom(obj.getClass())) { @SuppressWarnings("unchecked") final E enumObj = (E) obj; return enumObj; } final String strObj = obj.toString().trim(); try { // Efficient but case-sensitive lookup in the constant map return Enum.valueOf(enumClass, strObj); } catch (IllegalArgumentException ex) { // fall through } // Try a case-insensitive lookup for (E value : enumClass.getEnumConstants()) { if (value.name().equalsIgnoreCase(strObj)) { return value; } } return defaultValue; } /** * Coerce the specified object to the specified pseudo-enum type using the * supplied pseudo-enum dictionary class. * * @param obj Object to coerce * @param pseudoEnumClass The pseudo-enum class * @param dictionaryClass Pseudo-enum dictionary class to look in * @param defaultValue Value to return if lookup fails * @param pseudo-enum type * @return Coerced value or default if coercion fails */ public static T toPseudoEnum(@Nullable Object obj, Class pseudoEnumClass, Class dictionaryClass, T defaultValue) { Objects.requireNonNull(pseudoEnumClass, "pseudoEnumClass"); Objects.requireNonNull(dictionaryClass, "dictionaryClass"); Objects.requireNonNull(defaultValue, "defaultValue"); if (obj == null) { return defaultValue; } if (pseudoEnumClass.isAssignableFrom(obj.getClass())) { @SuppressWarnings("unchecked") final T enumObj = (T) obj; return enumObj; } final String strObj = obj.toString().trim(); try { for (Field field : dictionaryClass.getFields()) { if ((field.getModifiers() & Modifier.STATIC) != 0 && pseudoEnumClass.isAssignableFrom(field.getType())) { final String fieldName = field.getName(); @SuppressWarnings("unchecked") final T entry = (T) field.get(null); if (strObj.equalsIgnoreCase(fieldName)) { return entry; } } } } catch (Exception ex) { // well that went badly } return defaultValue; } /** * Coerce the supplied object to a Vector2i. * * @param obj Object to coerce * @return Vector2i, returns Vector2i.ZERO if coercion failed */ public static Vector2i toVector2i(@Nullable Object obj) { if (obj == null) { return Vector2i.ZERO; } if (obj instanceof Vectorl) { obj = ((Vectorl) obj).toInt(); } else if (obj instanceof Vectorf) { obj = ((Vectorf) obj).toInt(); } else if (obj instanceof Vectord) { obj = ((Vectord) obj).toInt(); } if (obj instanceof Vector2i) { return (Vector2i) obj; } else if (obj instanceof Vector3i) { return new Vector2i((Vector3i) obj); } else if (obj instanceof Vector4i) { return new Vector2i((Vector4i) obj); } else if (obj instanceof VectorNi) { return new Vector2i((VectorNi) obj); } final Matcher vecMatch = Coerce.vector2Pattern.matcher(obj.toString()); if (Coerce.listBracketsMatch(vecMatch)) { return new Vector2i(Integer.parseInt(vecMatch.group(1)), Integer.parseInt(vecMatch.group(2))); } final List list = Coerce.toList(obj); if (list.size() == 2) { return new Vector2i(Coerce.toInteger(list.get(0)), Coerce.toInteger(list.get(1))); } return Vector2i.ZERO; } /** * Sanitise a string containing a common representation of a number to make * it parsable. Strips thousand-separating commas and trims later members * of a comma-separated list. For example the string "(9.5, 10.6, 33.2)" * will be sanitised to "9.5". * * @param obj Object to sanitise * @return Sanitised number-format string to parse */ private static String sanitiseNumber(Object obj) { String string = obj.toString().trim(); if (string.length() < 1) { return "0"; } final Matcher candidate = Coerce.listPattern.matcher(string); if (Coerce.listBracketsMatch(candidate)) { string = candidate.group(2).trim(); } final int decimal = string.indexOf('.'); final int comma = string.indexOf(',', decimal); if (decimal > -1 && comma > -1) { return Coerce.sanitiseNumber(string.substring(0, comma)); } if (string.indexOf('-', 1) != -1) { return "0"; } return string.replace(",", "").split(" ", 0)[0]; } private static boolean listBracketsMatch(Matcher candidate) { return candidate.matches() && Coerce.listPairings[0].indexOf(candidate.group(1)) == Coerce.listPairings[1].indexOf(candidate.group(3)); } private static List primitiveArrayToList(Object obj) { if (obj instanceof boolean[]) { return Booleans.asList((boolean[]) obj); } else if (obj instanceof char[]) { return Chars.asList((char[]) obj); } else if (obj instanceof byte[]) { return Bytes.asList((byte[]) obj); } else if (obj instanceof short[]) { return Shorts.asList((short[]) obj); } else if (obj instanceof int[]) { return Ints.asList((int[]) obj); } else if (obj instanceof long[]) { return Longs.asList((long[]) obj); } else if (obj instanceof float[]) { return Floats.asList((float[]) obj); } else if (obj instanceof double[]) { return Doubles.asList((double[]) obj); } return Collections.emptyList(); } private static List parseStringToList(String string) { final Matcher candidate = Coerce.listPattern.matcher(string); if (!Coerce.listBracketsMatch(candidate)) { return Collections.emptyList(); } final List list = Lists.newArrayList(); for (final String part : candidate.group(2).split(",", -1)) { list.add(part); } return list; } }