    001/**
    002 *   Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
    003 */
    004package com.typesafe.config.impl;
    005
    006import java.io.DataOutputStream;
    007import java.io.File;
    008import java.io.IOException;
    009import java.io.ObjectInputStream;
    010import java.io.ObjectOutputStream;
    011import java.net.URISyntaxException;
    012import java.net.URL;
    013import java.util.ArrayList;
    014import java.util.List;
    015
    016import com.typesafe.config.ConfigException;
    017import com.typesafe.config.ConfigOrigin;
    018import com.typesafe.config.ConfigSyntax;
    019
    020/**
    021 * Internal implementation detail, not ABI stable, do not touch.
    022 * For use only by the {@link com.typesafe.config} package.
    023 */
    024final public class ConfigImplUtil {
    025    static boolean equalsHandlingNull(Object a, Object b) {
    026        if (a == null && b != null)
    027            return false;
    028        else if (a != null && b == null)
    029            return false;
    030        else if (a == b) // catches null == null plus optimizes identity case
    031            return true;
    032        else
    033            return a.equals(b);
    034    }
    035
    036    static boolean isC0Control(int codepoint) {
    037      return (codepoint >= 0x0000 && codepoint <= 0x001F);
    038    }
    039
    040    public static String renderJsonString(String s) {
    041        StringBuilder sb = new StringBuilder();
    042        sb.append('"');
    043        for (int i = 0; i < s.length(); ++i) {
    044            char c = s.charAt(i);
    045            switch (c) {
    046            case '"':
    047                sb.append("\\\"");
    048                break;
    049            case '\\':
    050                sb.append("\\\\");
    051                break;
    052            case '\n':
    053                sb.append("\\n");
    054                break;
    055            case '\b':
    056                sb.append("\\b");
    057                break;
    058            case '\f':
    059                sb.append("\\f");
    060                break;
    061            case '\r':
    062                sb.append("\\r");
    063                break;
    064            case '\t':
    065                sb.append("\\t");
    066                break;
    067            default:
    068                if (isC0Control(c))
    069                    sb.append(String.format("\\u%04x", (int) c));
    070                else
    071                    sb.append(c);
    072            }
    073        }
    074        sb.append('"');
    075        return sb.toString();
    076    }
    077
    078    static String renderStringUnquotedIfPossible(String s) {
    079        // this can quote unnecessarily as long as it never fails to quote when
    080        // necessary
    081        if (s.length() == 0)
    082            return renderJsonString(s);
    083
    084        // if it starts with a hyphen or number, we have to quote
    085        // to ensure we end up with a string and not a number
    086        int first = s.codePointAt(0);
    087        if (Character.isDigit(first) || first == '-')
    088            return renderJsonString(s);
    089
    090        if (s.startsWith("include") || s.startsWith("true") || s.startsWith("false")
    091                || s.startsWith("null") || s.contains("//"))
    092            return renderJsonString(s);
    093
    094        // only unquote if it's pure alphanumeric
    095        for (int i = 0; i < s.length(); ++i) {
    096            char c = s.charAt(i);
    097            if (!(Character.isLetter(c) || Character.isDigit(c) || c == '-'))
    098                return renderJsonString(s);
    099        }
    100
    101        return s;
    102    }
    103
    104    static boolean isWhitespace(int codepoint) {
    105        switch (codepoint) {
    106        // try to hit the most common ASCII ones first, then the nonbreaking
    107        // spaces that Java brokenly leaves out of isWhitespace.
    108        case ' ':
    109        case '\n':
    110        case '\u00A0':
    111        case '\u2007':
    112        case '\u202F':
    113            // this one is the BOM, see
    114            // http://www.unicode.org/faq/utf_bom.html#BOM
    115            // we just accept it as a zero-width nonbreaking space.
    116        case '\uFEFF':
    117            return true;
    118        default:
    119            return Character.isWhitespace(codepoint);
    120        }
    121    }
    122
    123    public static String unicodeTrim(String s) {
    124        // this is dumb because it looks like there aren't any whitespace
    125        // characters that need surrogate encoding. But, points for
    126        // pedantic correctness! It's future-proof or something.
    127        // String.trim() actually is broken, since there are plenty of
    128        // non-ASCII whitespace characters.
    129        final int length = s.length();
    130        if (length == 0)
    131            return s;
    132
    133        int start = 0;
    134        while (start < length) {
    135            char c = s.charAt(start);
    136            if (c == ' ' || c == '\n') {
    137                start += 1;
    138            } else {
    139                int cp = s.codePointAt(start);
    140                if (isWhitespace(cp))
    141                    start += Character.charCount(cp);
    142                else
    143                    break;
    144            }
    145        }
    146
    147        int end = length;
    148        while (end > start) {
    149            char c = s.charAt(end - 1);
    150            if (c == ' ' || c == '\n') {
    151                --end;
    152            } else {
    153                int cp;
    154                int delta;
    155                if (Character.isLowSurrogate(c)) {
    156                    cp = s.codePointAt(end - 2);
    157                    delta = 2;
    158                } else {
    159                    cp = s.codePointAt(end - 1);
    160                    delta = 1;
    161                }
    162                if (isWhitespace(cp))
    163                    end -= delta;
    164                else
    165                    break;
    166            }
    167        }
    168        return s.substring(start, end);
    169    }
    170
    171
    172    public static ConfigException extractInitializerError(ExceptionInInitializerError e) {
    173        Throwable cause = e.getCause();
    174        if (cause != null && cause instanceof ConfigException) {
    175            return (ConfigException) cause;
    176        } else {
    177            throw e;
    178        }
    179    }
    180
    181    static File urlToFile(URL url) {
    182        // this isn't really right, clearly, but not sure what to do.
    183        try {
    184            // this will properly handle hex escapes, etc.
    185            return new File(url.toURI());
    186        } catch (URISyntaxException e) {
    187            // this handles some stuff like file:///c:/Whatever/
    188            // apparently but mangles handling of hex escapes
    189            return new File(url.getPath());
    190        } catch (IllegalArgumentException e) {
    191            // file://foo with double slash causes
    192            // IllegalArgumentException "url has an authority component"
    193            return new File(url.getPath());
    194        }
    195    }
    196
    197    public static String joinPath(String... elements) {
    198        return (new Path(elements)).render();
    199    }
    200
    201    public static String joinPath(List<String> elements) {
    202        return joinPath(elements.toArray(new String[0]));
    203    }
    204
    205    public static List<String> splitPath(String path) {
    206        Path p = Path.newPath(path);
    207        List<String> elements = new ArrayList<String>();
    208        while (p != null) {
    209            elements.add(p.first());
    210            p = p.remainder();
    211        }
    212        return elements;
    213    }
    214
    215    public static ConfigOrigin readOrigin(ObjectInputStream in) throws IOException {
    216        return SerializedConfigValue.readOrigin(in, null);
    217    }
    218
    219    public static void writeOrigin(ObjectOutputStream out, ConfigOrigin origin) throws IOException {
    220        SerializedConfigValue.writeOrigin(new DataOutputStream(out), (SimpleConfigOrigin) origin,
    221                null);
    222    }
    223
    224    static String toCamelCase(String originalName) {
    225        String[] words = originalName.split("-+");
    226        StringBuilder nameBuilder = new StringBuilder(originalName.length());
    227        for (String word : words) {
    228            if (nameBuilder.length() == 0) {
    229                nameBuilder.append(word);
    230            } else {
    231                nameBuilder.append(word.substring(0, 1).toUpperCase());
    232                nameBuilder.append(word.substring(1));
    233            }
    234        }
    235        return nameBuilder.toString();
    236    }
    237
    238    private static char underscoreMappings(int num) {
    239        // Rationale on name mangling:
    240        //
    241        // Most shells (e.g. bash, sh, etc.) doesn't support any character other
    242        // than alphanumeric and `_` in environment variables names.
    243        // In HOCON the default separator is `.` so it is directly translated to a
    244        // single `_` for convenience; `-` and `_` are less often present in config
    245        // keys but they have to be representable and the only possible mapping is
    246        // `_` repeated.
    247        switch (num) {
    248            case 1: return '.';
    249            case 2: return '-';
    250            case 3: return '_';
    251            default: return 0;
    252        }
    253    }
    254
    255    static String envVariableAsProperty(String variable, String prefix) throws ConfigException {
    256        StringBuilder builder = new StringBuilder();
    257
    258        String strippedPrefix = variable.substring(prefix.length(), variable.length());
    259
    260        int underscores = 0;
    261        for (char c : strippedPrefix.toCharArray()) {
    262            if (c == '_') {
    263                underscores++;
    264            } else {
    265                if (underscores > 0  && underscores < 4) {
    266                    builder.append(underscoreMappings(underscores));
    267                } else if (underscores > 3) {
    268                    throw new ConfigException.BadPath(variable, "Environment variable contains an un-mapped number of underscores.");
    269                }
    270                underscores = 0;
    271                builder.append(c);
    272            }
    273        }
    274
    275        if (underscores > 0  && underscores < 4) {
    276            builder.append(underscoreMappings(underscores));
    277        } else if (underscores > 3) {
    278            throw new ConfigException.BadPath(variable, "Environment variable contains an un-mapped number of underscores.");
    279        }
    280
    281        return builder.toString();
    282    }
    283
    284    /**
    285     * Guess configuration syntax from given filename.
    286     *
    287     * @param filename configuration filename
    288     * @return configuration syntax if a match is found. Otherwise, null.
    289     */
    290    public static ConfigSyntax syntaxFromExtension(String filename) {
    291        if (filename == null)
    292            return null;
    293        else if (filename.endsWith(".json"))
    294            return ConfigSyntax.JSON;
    295        else if (filename.endsWith(".conf"))
    296            return ConfigSyntax.CONF;
    297        else if (filename.endsWith(".properties"))
    298            return ConfigSyntax.PROPERTIES;
    299        else
    300            return null;
    301    }
    302}