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

com.threerings.getdown.util.Config Maven / Gradle / Ivy

There is a newer version: 1.8.7
Show newest version
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE

package com.threerings.getdown.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static com.threerings.getdown.Log.log;

/**
 * Handles parsing and runtime access for Getdown's config files (mainly {@code getdown.txt}).
 * These files contain zero or more mappings for a particular string key. Config values can be
 * fetched as single strings, lists of strings, or parsed into primitives or compound data types
 * like colors and rectangles.
 */
public class Config
{
    /** Empty configuration. */
    public static final Config EMPTY = new Config(new HashMap());

    /** Options that control the {@link #parsePairs} function. */
    public static class ParseOpts {
        // these should be tweaked as desired by the caller
        public boolean biasToKey = false;
        public boolean strictComments = false;

        // these are filled in by parseConfig
        public String osname = null;
        public String osarch = null;
    }

    /**
     * Creates a parse configuration, filling in the platform filters (or not) depending on the
     * value of {@code checkPlatform}.
     */
    public static ParseOpts createOpts (boolean checkPlatform) {
        ParseOpts opts = new ParseOpts();
        if (checkPlatform) {
            opts.osname = StringUtil.deNull(System.getProperty("os.name")).toLowerCase(Locale.ROOT);
            opts.osarch = StringUtil.deNull(System.getProperty("os.arch")).toLowerCase(Locale.ROOT);
        }
        return opts;
    }

    /**
     * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
     * encoding.
     *
     * @param opts options that influence the parsing. See {@link #createOpts}.
     *
     * @return a list of String[] instances containing the key/value pairs in the
     * order they were parsed from the file.
     */
    public static List parsePairs (File source, ParseOpts opts)
        throws IOException
    {
        // annoyingly FileReader does not allow encoding to be specified (uses platform default)
        try (FileInputStream fis = new FileInputStream(source);
             InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
            return parsePairs(input, opts);
        }
    }

    /**
     * See {@link #parsePairs(File,ParseOpts)}.
     */
    public static List parsePairs (Reader source, ParseOpts opts) throws IOException
    {
        List pairs = new ArrayList<>();
        for (String line : FileUtil.readLines(source)) {
            // nix comments
            int cidx = line.indexOf("#");
            if (opts.strictComments ? cidx == 0 : cidx != -1) {
                line = line.substring(0, cidx);
            }

            // trim whitespace and skip blank lines
            line = line.trim();
            if (StringUtil.isBlank(line)) {
                continue;
            }

            // parse our key/value pair
            String[] pair = new String[2];
            // if we're biasing toward key, put all the extra = in the key rather than the value
            int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
            if (eidx != -1) {
                pair[0] = line.substring(0, eidx).trim();
                pair[1] = line.substring(eidx+1).trim();
            } else {
                pair[0] = line;
                pair[1] = "";
            }

            // if the pair has an os qualifier, we need to process it
            if (pair[1].startsWith("[")) {
                int qidx = pair[1].indexOf("]");
                if (qidx == -1) {
                    log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
                    continue; // omit the pair entirely
                }
                // if we're checking qualifiers and the os doesn't match this qualifier, skip it
                String quals = pair[1].substring(1, qidx);
                if (opts.osname != null && !checkQualifiers(quals, opts.osname, opts.osarch)) {
                    log.debug("Skipping", "quals", quals,
                              "osname", opts.osname, "osarch", opts.osarch,
                              "key", pair[0], "value", pair[1]);
                    continue;
                }
                // otherwise filter out the qualifier text
                pair[1] = pair[1].substring(qidx+1).trim();
            }

            pairs.add(pair);
        }

        return pairs;
    }

    /**
     * Takes a comma-separated String of four integers and returns a rectangle using those ints as
     * the its x, y, width, and height.
     */
    public static Rectangle parseRect (String name, String value)
    {
        if (!StringUtil.isBlank(value)) {
            int[] v = StringUtil.parseIntArray(value);
            if (v != null && v.length == 4) {
                return new Rectangle(v[0], v[1], v[2], v[3]);
            }
            log.warning("Ignoring invalid rect '" + name + "' config '" + value + "'.");
        }
        return null;
    }

    /**
     * Parses the given hex color value (e.g. FFCC99) and returns an {@code Integer} with that
     * value. If the given value is null or not a valid hexadecimal number, this will return null.
     */
    public static Integer parseColor (String hexValue)
    {
        if (!StringUtil.isBlank(hexValue)) {
            try {
                // if no alpha channel is specified, use 255 (full alpha)
                int alpha = hexValue.length() > 6 ? 0 : 0xFF000000;
                return Integer.parseInt(hexValue, 16) | alpha;
            } catch (NumberFormatException e) {
                log.warning("Ignoring invalid color", "hexValue", hexValue, "exception", e);
            }
        }
        return null;
    }

    /**
     * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
     * encoding.
     *
     * @return a map from keys to values, where a value will be an array of strings if more than
     * one key/value pair in the config file was associated with the same key.
     */
    public static Config parseConfig (File source, ParseOpts opts)
        throws IOException
    {
        Map data = new HashMap<>();

        // I thought that we could use HashMap and put new String[] {pair[1]} for
        // the null case, but it mysteriously dies on launch, so leaving it as HashMap for now
        for (String[] pair : parsePairs(source, opts)) {
            Object value = data.get(pair[0]);
            if (value == null) {
                data.put(pair[0], pair[1]);
            } else if (value instanceof String) {
                data.put(pair[0], new String[] { (String)value, pair[1] });
            } else if (value instanceof String[]) {
                String[] values = (String[])value;
                String[] nvalues = new String[values.length+1];
                System.arraycopy(values, 0, nvalues, 0, values.length);
                nvalues[values.length] = pair[1];
                data.put(pair[0], nvalues);
            }
        }

        // special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
        // true' then we reparse the file with strict comments (i.e. # is only assumed to start a
        // comment in column 0)
        if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
            opts.strictComments = true;
            return parseConfig(source, opts);
        }

        return new Config(data);
    }

    public Config (Map data) {
        _data = data;
    }

    /**
     * Returns whether {@code name} has a value in this config.
     */
    public boolean hasValue (String name) {
        return _data.containsKey(name);
    }

    /**
     * Returns the raw-value for {@code name}. This may be a {@code String}, {@code String[]}, or
     * {@code null}.
     */
    public Object getRaw (String name) {
        return _data.get(name);
    }

    /**
     * Returns the specified config value as a string, or {@code null}.
     */
    public String getString (String name) {
        return (String)_data.get(name);
    }

    /**
     * Returns the specified config value as a string, or {@code def}.
     */
    public String getString (String name, String def) {
        String value = (String)_data.get(name);
        return value == null ? def : value;
    }

    /**
     * Returns the specified config value as a boolean.
     */
    public boolean getBoolean (String name) {
        return Boolean.parseBoolean(getString(name));
    }

    /**
     * Massages a single string into an array and leaves existing array values as is. Simplifies
     * access to parameters that are expected to be arrays.
     */
    public String[] getMultiValue (String name)
    {
        Object value = _data.get(name);
        if (value instanceof String) {
            return new String[] { (String)value };
        } else {
            return (String[])value;
        }
    }

    /** Used to parse rectangle specifications from the config file. */
    public Rectangle getRect (String name, Rectangle def)
    {
        String value = getString(name);
        Rectangle rect = parseRect(name, value);
        return (rect == null) ? def : rect;
    }

    /**
     * Parses and returns the config value for {@code name} as an int. If no value is provided,
     * {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
     * returned.
     */
    public int getInt (String name, int def) {
        String value = getString(name);
        try {
            return value == null ? def : Integer.parseInt(value);
        } catch (Exception e) {
            log.warning("Ignoring invalid int '" + name + "' config '" + value + "',");
            return def;
        }
    }

    /**
     * Parses and returns the config value for {@code name} as a long. If no value is provided,
     * {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
     * returned.
     */
    public long getLong (String name, long def) {
        String value = getString(name);
        try {
            return value == null ? def : Long.parseLong(value);
        } catch (Exception e) {
            log.warning("Ignoring invalid long '" + name + "' config '" + value + "',");
            return def;
        }
    }

    /** Used to parse color specifications from the config file. */
    public int getColor (String name, int def)
    {
        String value = getString(name);
        Integer color = parseColor(value);
        return (color == null) ? def : color;
    }

    /** Parses a list of strings from the config file. */
    public String[] getList (String name)
    {
        String value = getString(name);
        return (value == null) ? new String[0] : StringUtil.parseStringArray(value);
    }

    /**
     * Parses a URL from the config file, checking first for a localized version.
     */
    public String getUrl (String name, String def)
    {
        String value = getString(name + "." + Locale.getDefault().getLanguage());
        if (StringUtil.isBlank(value)) {
            value = getString(name);
        }
        if (StringUtil.isBlank(value)) {
            value = def;
        }
        if (!StringUtil.isBlank(value)) {
            try {
                HostWhitelist.verify(new URL(value));
            } catch (MalformedURLException e) {
                log.warning("Invalid URL.", "url", value, e);
                value = null;
            }
        }
        return value;
    }

    /**
     * A helper function for {@link #parsePairs(Reader,ParseOpts)}. Qualifiers have the following
     * form:
     * 
     * id = os[-arch]
     * ids = id | id,ids
     * quals = !id | ids
     * 
* Examples: [linux-amd64,linux-x86_64], [windows], [mac os x], [!windows]. Negative qualifiers * must appear alone, they cannot be used with other qualifiers (positive or negative). */ protected static boolean checkQualifiers (String quals, String osname, String osarch) { if (quals.startsWith("!")) { if (quals.indexOf(",") != -1) { // sanity check log.warning("Multiple qualifiers cannot be used when one of the qualifiers " + "is negative", "quals", quals); return false; } return !checkQualifier(quals.substring(1), osname, osarch); } for (String qual : quals.split(",")) { if (checkQualifier(qual, osname, osarch)) { return true; // if we have a positive match, we can immediately return true } } return false; // we had no positive matches, so return false } /** A helper function for {@link #checkQualifiers}. */ protected static boolean checkQualifier (String qual, String osname, String osarch) { String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-"); String os = bits[0], arch = (bits.length > 1) ? bits[1] : ""; return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1); } private final Map _data; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy