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}