| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.freemarker.docgen; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.math.BigDecimal; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Evaluates CJSON language expressions. Regarding the CJSON language see |
| * the JavaDocs of {@link Transform}. |
| */ |
| /* |
| * This is just quick-n-dirty copy-paste re-mix of the TDD interpreter from |
| * my aged project, FMPP, but slightly modified, as outlined in the description |
| * of the language. |
| */ |
| final class CJSONInterpreter { |
| |
| /** |
| * Evaluates function calls to itself. |
| */ |
| public static final EvaluationEnvironment SIMPLE_EVALUATION_ENVIRONMENT |
| = new EvaluationEnvironment() { |
| public Object evalFunctionCall(FunctionCall f, CJSONInterpreter ip) { |
| return f; |
| } |
| |
| public Object notify( |
| EvaluationEvent event, |
| CJSONInterpreter ip, String name, Object extra) { |
| return null; |
| } |
| }; |
| |
| /** For accelerating {@link #isUnquotedStringChar(char)}. */ |
| private static final boolean[] UQSTR_CHARS = { |
| false, // NUL (0) |
| false, // SOH (1) |
| false, // STX (2) |
| false, // ETX (3) |
| false, // EOT (4) |
| false, // ENQ (5) |
| false, // ACK (6) |
| false, // BEL (7) |
| false, // BS (8) |
| false, // HT (9) |
| false, // LF (10) |
| false, // VT (11) |
| false, // FF (12) |
| false, // CR (13) |
| false, // SO (14) |
| false, // SI (15) |
| false, // DLE (16) |
| false, // DC1 (17) |
| false, // DC2 (18) |
| false, // DC2 (19) |
| false, // DC4 (20) |
| false, // NAK (21) |
| false, // SYN (22) |
| false, // ETB (23) |
| false, // CAN (24) |
| false, // EM (25) |
| false, // SUB (26) |
| false, // ESC (27) |
| false, // FS (28) |
| false, // GS (29) |
| false, // RS (30) |
| false, // US (31) |
| false, // SP (32) |
| false, // ! (33) |
| false, // " (34) |
| false, // # (35) |
| true, // $ (36) |
| false, // % (37) |
| false, // & (38) |
| false, // ' (39) |
| false, // ( (40) |
| false, // ) (41) |
| false, // * (42) |
| false, // + (43) |
| false, // , (44) |
| true, // - (45) |
| true, // . (46) |
| false, // / (47) |
| true, // 0 (48) |
| true, // 1 (49) |
| true, // 2 (50) |
| true, // 3 (51) |
| true, // 4 (52) |
| true, // 5 (53) |
| true, // 6 (54) |
| true, // 7 (55) |
| true, // 8 (56) |
| true, // 9 (57) |
| false, // : (58) |
| false, // ; (59) |
| false, // < (60) |
| false, // = (61) |
| false, // > (62) |
| false, // ? (63) |
| true, // @ (64) |
| true, // A (65) |
| true, // B (66) |
| true, // C (67) |
| true, // D (68) |
| true, // E (69) |
| true, // F (70) |
| true, // G (71) |
| true, // H (72) |
| true, // I (73) |
| true, // J (74) |
| true, // K (75) |
| true, // L (76) |
| true, // M (77) |
| true, // N (78) |
| true, // O (79) |
| true, // P (80) |
| true, // Q (81) |
| true, // R (82) |
| true, // S (83) |
| true, // T (84) |
| true, // U (85) |
| true, // V (86) |
| true, // W (87) |
| true, // X (88) |
| true, // Y (89) |
| true, // Z (90) |
| false, // [ (91) |
| false, // \ (92) |
| false, // ] (93) |
| false, // ^ (94) |
| true, // _ (95) |
| false, // ` (96) |
| true, // a (97) |
| true, // b (98) |
| true, // c (99) |
| true, // d (100) |
| true, // e (101) |
| true, // f (102) |
| true, // g (103) |
| true, // h (104) |
| true, // i (105) |
| true, // j (106) |
| true, // k (107) |
| true, // l (108) |
| true, // m (109) |
| true, // n (110) |
| true, // o (111) |
| true, // p (112) |
| true, // q (113) |
| true, // r (114) |
| true, // s (115) |
| true, // t (116) |
| true, // u (117) |
| true, // v (118) |
| true, // w (119) |
| true, // x (120) |
| true, // y (121) |
| true, // z (122) |
| false, // { (123) |
| false, // | (124) |
| false, // } (125) |
| false, // ~ (126) |
| false // DEL (127) |
| }; |
| |
| private int p; |
| private int ln; |
| private EvaluationEnvironment ee; |
| private String tx; |
| private String fileName; |
| private boolean skipWSFoundNL; |
| |
| // Can't be instantiated |
| private CJSONInterpreter() { |
| // Nop |
| } |
| |
| // ------------------------------------------------------------------------- |
| // Public static methods |
| |
| /** |
| * Evaluates text as single CJSON expression. |
| * |
| * @param text the text to interpret. |
| * @param ee the {@link EvaluationEnvironment} used to resolve function |
| * calls. If it is <code>null</code> then |
| * {@link #SIMPLE_EVALUATION_ENVIRONMENT} will be used. |
| * @param forceStringValues specifies if expressions as <tt>true</tt> and |
| * <tt>123</tt> should be interpreted as strings, or as boolean and |
| * number respectively. |
| * @param fileName the path of the source file, or other description of the |
| * source. It is used for informative purposes only, as in error |
| * messages. |
| * |
| * @return the result of the evaluation. Possibly an empty |
| * <code>Map</code>, but never <code>null</code>. |
| */ |
| public static Object eval( |
| String text, EvaluationEnvironment ee, boolean forceStringValues, |
| String fileName) throws EvaluationException { |
| CJSONInterpreter ip = new CJSONInterpreter(); |
| ip.init(text, fileName, ee); |
| ip.skipWS(); |
| if (ip.p == ip.ln) { |
| throw ip.newSyntaxError("The text is empty."); |
| } |
| Object res = ip.fetchExpression(forceStringValues, false); |
| ip.skipWS(); |
| if (ip.p < ip.ln) { |
| throw ip.newSyntaxError("Extra character(s) after the expression."); |
| } |
| return res; |
| } |
| |
| /** |
| * Evaluates a {@link Fragment} as single CJSON expression. The expression |
| * can be surrounded with superfluous white-space. |
| * |
| * @see #eval(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Object eval( |
| Fragment fragment, |
| EvaluationEnvironment ee, boolean forceStringValues) |
| throws EvaluationException { |
| CJSONInterpreter ip = new CJSONInterpreter(); |
| ip.init(fragment, ee); |
| ip.skipWS(); |
| if (ip.p == ip.ln) { |
| throw ip.newSyntaxError("The text is empty."); |
| } |
| Object res = ip.fetchExpression(forceStringValues, false); |
| ip.skipWS(); |
| if (ip.p < ip.ln) { |
| throw ip.newSyntaxError("Extra character(s) after the expression."); |
| } |
| return res; |
| } |
| |
| /** |
| * Same as <code>eval(text, null, false, fileName)</code>. |
| * @see #eval(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Object eval(String text, String fileName) |
| throws EvaluationException { |
| return eval(text, null, false, fileName); |
| } |
| |
| /** |
| * Same as <code>eval(text, null, false, null)</code>. |
| * @see #eval(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Object eval(String text) |
| throws EvaluationException { |
| return eval(text, null, false, null); |
| } |
| |
| /** |
| * Evaluates text as a list of key:value pairs. |
| * |
| * @param text the text to interpret. |
| * @param ee the {@link EvaluationEnvironment} used to resolve function |
| * calls. If it is <code>null</code> then |
| * {@link #SIMPLE_EVALUATION_ENVIRONMENT} will be used. |
| * @param forceStringValues specifies if expressions as <tt>true</tt> and |
| * <tt>123</tt> should be interpreted as strings, or as boolean and |
| * number respectively. |
| * @param fileName the path of the source file, or other description of the |
| * source. It is used for informative purposes only, as in error |
| * messages. |
| * |
| * @return the result of the evaluation. Possibly an empty |
| * <code>Map</code>, but never <code>null</code>. The entries in the |
| * map are guaranteed to be in the same order as they were defined in |
| * the CJSON expression. |
| */ |
| public static Map<String, Object> evalAsMap( |
| String text, EvaluationEnvironment ee, boolean forceStringValues, |
| String fileName) throws EvaluationException { |
| CJSONInterpreter ip = new CJSONInterpreter(); |
| ip.init(text, fileName, ee); |
| Map<String, Object> res = new LinkedHashMap<String, Object>(); |
| boolean done = false; |
| try { |
| try { |
| ip.ee.notify( |
| EvaluationEvent.ENTER_MAP, |
| ip, null, res); |
| done = true; |
| } catch (Throwable e) { |
| throw ip.newWrappedError(e); |
| } |
| return ip.fetchMapInner(res, (char) 0x20, forceStringValues); |
| } finally { |
| if (done) { |
| try { |
| ip.ee.notify( |
| EvaluationEvent.LEAVE_MAP, |
| ip, null, res); |
| } catch (Throwable e) { |
| throw ip.newWrappedError(e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Same as <code>evalAsMap(textFromUTF8File, null, false, null)</code>. |
| * The file must use UTF-8 encoding. Initial BOM is allowed. |
| * @throws IOException |
| * @see #evalAsMap(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Map<String, Object> evalAsMap(File f) |
| throws EvaluationException, IOException { |
| String s; |
| InputStream in = new FileInputStream(f); |
| try { |
| s = loadCJSONFile(in, f.getAbsolutePath()); |
| } finally { |
| in.close(); |
| } |
| return evalAsMap(s, f.getAbsolutePath()); |
| } |
| |
| /** |
| * Same as <code>evalAsMap(text, null, false, null)</code>. |
| * @see #evalAsMap(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Map<String, Object> evalAsMap(String text) |
| throws EvaluationException { |
| return evalAsMap(text, null, false, null); |
| } |
| |
| /** |
| * Same as <code>evalAsMap(text, null, false, fileName)</code>. |
| * @see #evalAsMap(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static Map<String, Object> evalAsMap(String text, String fileName) |
| throws EvaluationException { |
| return evalAsMap(text, null, false, fileName); |
| } |
| |
| /** |
| * Evaluates text as a list values. |
| * |
| * @param text the text to interpret. |
| * @param ee the {@link EvaluationEnvironment} used to resolve function |
| * calls. If it is <code>null</code> then |
| * {@link #SIMPLE_EVALUATION_ENVIRONMENT} will be used. |
| * @param forceStringValues specifies if expressions as <tt>true</tt> and |
| * <tt>123</tt> should be interpreted as strings, or as boolean and |
| * number respectively. |
| * @param fileName the path of the source file, or other description of the |
| * source. It is used for informative purposes only, as in error |
| * messages. |
| * |
| * @return the result of the evaluation. Possibly an empty |
| * <code>List</code>, but never <code>null</code>. |
| */ |
| public static List<Object> evalAsList( |
| String text, EvaluationEnvironment ee, boolean forceStringValues, |
| String fileName) throws EvaluationException { |
| CJSONInterpreter ip = new CJSONInterpreter(); |
| ip.init(text, fileName, ee); |
| List<Object> res = new ArrayList<Object>(); |
| boolean done = false; |
| try { |
| try { |
| ip.ee.notify( |
| EvaluationEvent.ENTER_LIST, |
| ip, null, res); |
| done = true; |
| } catch (Throwable e) { |
| throw ip.newWrappedError(e); |
| } |
| return ip.fetchListInner(res, (char) 0x20, forceStringValues); |
| } finally { |
| if (done) { |
| try { |
| ip.ee.notify( |
| EvaluationEvent.LEAVE_LIST, |
| ip, null, res); |
| } catch (Throwable e) { |
| throw ip.newWrappedError(e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Same as <code>evalAsList(text, null, false, null)</code>. |
| * @see #evalAsList(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static List<Object> evalAsList(String text) |
| throws EvaluationException { |
| return evalAsList(text, null, false, null); |
| } |
| |
| /** |
| * Same as <code>evalAsList(text, null, false, fileName)</code>. |
| * @see #evalAsList(String, EvaluationEnvironment, boolean, String) |
| */ |
| public static List<Object> evalAsList(String text, String fileName) |
| throws EvaluationException { |
| return evalAsList(text, null, false, fileName); |
| } |
| |
| /** |
| * Loads a CJSON file with utilizing <tt>#encoding:<i>enc</i></tt> header. |
| * If the header is missing, UTF-8 will be used. |
| * @param in the stream that reads the content of the file. |
| * @param source the description of the location of the "file" (usually a |
| * path). Can be {@code null}. |
| */ |
| public static String loadCJSONFile(InputStream in, String source) |
| throws IOException { |
| byte[] b = loadByteArray(in); |
| return loadCJSONFile(b, source); |
| } |
| |
| /** |
| * Loads a CJSON file with utilizing <tt>#encoding:<i>enc</i></tt> header. |
| * If the header is missing, the encoding given as parameter is used. |
| * |
| * @param b the content of the "file". |
| * @param source the description of the location of the "file" (usually a |
| * path). Can be {@code null}. |
| */ |
| public static String loadCJSONFile(byte[] b, String source) |
| throws IOException { |
| String charset = extractCharsetComment(b); |
| try { |
| return new String(b, charset == null ? "UTF-8" : charset); |
| } catch (java.io.UnsupportedEncodingException e) { |
| String msg = "Unsupported character encoding, " |
| + TextUtil.jQuote(charset) + " was specifed in "; |
| if (source != null) { |
| msg += "this CJSON file: " + source; |
| } else { |
| msg += "the CJSON file."; |
| } |
| throw new IOException(msg); |
| } |
| } |
| |
| /** |
| * Converts an object to a CJSON-like representation (not necessary valid |
| * CJSON). |
| * @param value the object to convert |
| * @return the CJSON "source code". |
| */ |
| public static String dump(Object value) { |
| StringBuilder buf = new StringBuilder(); |
| dumpValue(buf, value, ""); |
| return buf.toString(); |
| } |
| |
| /** |
| * Returns the type-name of a value according to the CJSON language. |
| */ |
| public static String cjsonTypeOf(Object value) { |
| if (value instanceof String) { |
| return "string"; |
| } else if (value instanceof Number) { |
| return "number"; |
| } else if (value instanceof Boolean) { |
| return "boolean"; |
| } else if (value instanceof List<?>) { |
| return "list"; |
| } else if (value instanceof LinkedHashMap<?, ?>) { |
| return "map"; |
| } else if (value instanceof Map<?, ?>) { |
| return "map (unordered)"; |
| } else if (value instanceof FunctionCall) { |
| return "function call"; |
| } else { |
| if (value != null) { |
| return value.getClass().getName(); |
| } else { |
| return "null"; |
| } |
| } |
| } |
| |
| // ------------------------------------------------------------------------- |
| // Public non-static methods |
| |
| public int getPosition() { |
| return p; |
| } |
| |
| public String getText() { |
| return tx; |
| } |
| |
| public String getFileName() { |
| return fileName; |
| } |
| |
| public EvaluationEnvironment getEvaluationEnvironment() { |
| return ee; |
| } |
| |
| // ------------------------------------------------------------------------- |
| // Private |
| |
| /** |
| * Fetches comma separated expressions. The expressions may surrounded with |
| * superfluous WS. |
| * @param list destination list |
| * @param terminator The character that signals the end of the list. |
| * Use 0x20 for EOS. <code>p</code> will point to the terminator |
| * character when the method returns. |
| */ |
| private List<Object> fetchListInner( |
| List<Object> list, char terminator, boolean forceStringValues) |
| throws EvaluationException { |
| int listP = p - 1; |
| skipWS(); |
| if (terminator == 0x20) { |
| listP = p; |
| } |
| |
| while (true) { |
| char c; |
| if (p < ln) { |
| c = tx.charAt(p); |
| if (c == terminator) { |
| return list; |
| } |
| if (c == ',') { |
| throw newSyntaxError( |
| "List item is missing before the comma."); |
| } |
| } else { |
| if (terminator == 0x20) { |
| return list; |
| } else { |
| throw newSyntaxError("Reached the end of the text, " |
| + "but the list was not closed with " |
| + TextUtil.jQuoteOrName(terminator) + ".", |
| listP); |
| } |
| } |
| list.add(fetchExpression(forceStringValues, false)); |
| c = skipSeparator( |
| terminator, null, "This is a list, and not a map."); |
| if (c == terminator) { |
| return list; |
| } |
| } |
| } |
| |
| /** |
| * Fetches comma separated key:value pairs. The expressions can be |
| * surrounded with superflous WS. |
| * @param map destination map |
| * @param terminator The character that signals the end of the key:value |
| * pair list. |
| * Use 0x20 for EOS. <code>p</code> will point to the terminator |
| * character when the method returns. |
| */ |
| @SuppressWarnings("unchecked") |
| private Map<String, Object> fetchMapInner( |
| Map<String, Object> map, char terminator, boolean forceStringValues) |
| throws EvaluationException { |
| int p2; |
| |
| int mapP = p - 1; |
| skipWS(); |
| if (terminator == 0x20) { |
| mapP = p; |
| } |
| |
| // Key lookup |
| while (true) { |
| char c; |
| |
| if (p < ln) { |
| c = tx.charAt(p); |
| if (c == terminator) { |
| return map; |
| } |
| if (c == ',') { |
| throw newSyntaxError( |
| "Key-value pair is missing before the comma."); |
| } |
| } else { |
| if (terminator == 0x20) { |
| return map; |
| } else { |
| throw newSyntaxError("Reached the end of the text, " |
| + "but the map was not closed with " |
| + TextUtil.jQuoteOrName(terminator) + ".", |
| mapP); |
| } |
| } |
| |
| int keyP = p; |
| Object o1 = fetchExpression(false, true); |
| FunctionCall keyFunc; |
| if (o1 instanceof FunctionCall) { |
| keyFunc = (FunctionCall) o1; |
| try { |
| o1 = ee.evalFunctionCall(keyFunc, this); |
| } catch (Throwable e) { |
| throw newError("Failed to evaluate function " |
| + TextUtil.jQuote(keyFunc.getName()) + ".", |
| keyP, e); |
| } |
| } else { |
| keyFunc = null; |
| } |
| |
| c = skipSeparator(terminator, null, null); |
| if (c == ':') { |
| if (!(o1 instanceof String)) { |
| if (keyFunc != o1) { |
| throw newError( |
| "The key must be a String, but it is a(n) " |
| + cjsonTypeOf(o1) + ".", keyP); |
| } else { |
| throw newError( |
| "You can't use the function here, " |
| + "because it can't be evaluated " |
| + "in this context.", |
| keyP); |
| } |
| } |
| |
| if (p == ln) { |
| throw newSyntaxError( |
| "The key must be followed by a value because " |
| + "colon was used.", keyP); |
| } |
| |
| Object o2; |
| boolean done = false; |
| try { |
| Object nr; |
| try { |
| nr = ee.notify( |
| EvaluationEvent.ENTER_MAP_KEY, |
| this, (String) o1, null); |
| done = true; |
| } catch (Throwable e) { |
| throw newWrappedError(e, keyP); |
| } |
| if (nr == null) { |
| o2 = fetchExpression(forceStringValues, false); |
| map.put((String) o1, o2); |
| } else { |
| p2 = p; |
| skipExpression(); |
| if (nr == EvaluationEnvironment.RETURN_FRAGMENT) { |
| map.put((String) o1, |
| new Fragment(tx, p2, p, fileName)); |
| } |
| } |
| } finally { |
| if (done) { |
| try { |
| ee.notify( |
| EvaluationEvent.LEAVE_MAP_KEY, |
| this, (String) o1, null); |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| } |
| } |
| |
| c = skipSeparator(terminator, null, |
| "Colon is for separating the key from the value, " |
| + "and the value was alredy given previously."); |
| } else if (c == ',' || c == terminator || c == 0x20) { |
| if (keyFunc == null) { |
| if (o1 instanceof String) { |
| boolean done = false; |
| try { |
| Object nr; |
| try { |
| nr = ee.notify( |
| EvaluationEvent.ENTER_MAP_KEY, |
| this, (String) o1, null); |
| done = true; |
| } catch (Throwable e) { |
| throw newWrappedError(e, keyP); |
| } |
| if (nr == null |
| || nr == EvaluationEnvironment |
| .RETURN_FRAGMENT) { |
| map.put((String) o1, Boolean.TRUE); |
| } |
| } finally { |
| if (done) { |
| try { |
| ee.notify( |
| EvaluationEvent.LEAVE_MAP_KEY, |
| this, (String) o1, null); |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| } |
| } |
| } else if (o1 instanceof Map) { |
| map.putAll((Map<String, Object>) o1); |
| } else { |
| throw newError( |
| "This expression should be either a string " |
| + "or a map, but it is a(n) " |
| + cjsonTypeOf(o1) + ".", keyP); |
| } |
| } else { |
| if (o1 instanceof Map) { |
| map.putAll((Map<String, Object>) o1); |
| } else { |
| if (keyFunc == o1) { |
| throw newError( |
| "You can't use the function here, " |
| + "because it can't be evaluated " |
| + "in this context.", |
| keyP); |
| } else { |
| throw newError( |
| "Function doesn't evalute to a map, but " |
| + "to " + cjsonTypeOf(o1) |
| + ", so it can't be merged into the map.", |
| keyP); |
| } |
| } |
| } |
| } |
| if (c == terminator) { |
| return map; |
| } |
| } |
| } |
| |
| /** |
| * Fetches arbitrary expression. No surrounding superflous WS is allowed! |
| */ |
| private Object fetchExpression(boolean forceStr, boolean mapKey) |
| throws EvaluationException { |
| char c; |
| |
| if (p >= ln) { //!!a |
| throw new BugException("Calling fetchExpression when p >= ln."); |
| } |
| |
| c = tx.charAt(p); |
| |
| // JSON Object: |
| if (c == '{') { |
| Object nr; |
| p++; |
| Object res; |
| Map<String, Object> map = new LinkedHashMap<String, Object>(); |
| boolean done = false; |
| try { |
| try { |
| nr = ee.notify( |
| EvaluationEvent.ENTER_MAP, |
| this, null, map); |
| done = true; |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| if (nr == null) { |
| fetchMapInner(map, '}', forceStr); |
| res = map; |
| } else { |
| p--; |
| int p2 = p; |
| skipExpression(); |
| res = new Fragment(tx, p2, p, fileName); |
| p--; |
| } |
| } finally { |
| if (done) { |
| try { |
| ee.notify( |
| EvaluationEvent.LEAVE_MAP, |
| this, null, map); |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| } |
| } |
| p++; |
| return res; //! |
| } |
| |
| // JSON array: |
| if (c == '[') { |
| p++; |
| List<Object> res = new ArrayList<Object>(); |
| boolean done = false; |
| try { |
| try { |
| ee.notify( |
| EvaluationEvent.ENTER_LIST, |
| this, null, res); |
| done = true; |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| fetchListInner(res, ']', forceStr); |
| } finally { |
| if (done) { |
| try { |
| ee.notify( |
| EvaluationEvent.LEAVE_LIST, |
| this, null, res); |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| } |
| } |
| p++; |
| return res; //! |
| } |
| |
| int b = p; |
| |
| // Quoted string: |
| if (c == '"' || c == '\'') { |
| char q = c; |
| |
| p++; |
| while (p < ln) { |
| c = tx.charAt(p); |
| if (c == '\\') { |
| break; |
| } |
| p++; |
| if (c == q) { |
| return tx.substring(b + 1, p - 1); //! |
| } |
| } |
| if (p == ln) { |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } |
| |
| int bidx = b + 1; |
| StringBuilder buf = new StringBuilder(); |
| while (true) { |
| buf.append(tx.substring(bidx, p)); |
| if (p == ln - 1) { |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } |
| c = tx.charAt(p + 1); |
| switch (c) { |
| case '"': |
| buf.append('"'); |
| bidx = p + 2; |
| break; |
| case '\'': |
| buf.append('\''); |
| bidx = p + 2; |
| break; |
| case '\\': |
| buf.append('\\'); |
| bidx = p + 2; |
| break; |
| case 'n': |
| buf.append('\n'); |
| bidx = p + 2; |
| break; |
| case 'r': |
| buf.append('\r'); |
| bidx = p + 2; |
| break; |
| case 't': |
| buf.append('\t'); |
| bidx = p + 2; |
| break; |
| case 'f': |
| buf.append('\f'); |
| bidx = p + 2; |
| break; |
| case 'b': |
| buf.append('\b'); |
| bidx = p + 2; |
| break; |
| case 'g': |
| buf.append('>'); |
| bidx = p + 2; |
| break; |
| case 'l': |
| buf.append('<'); |
| bidx = p + 2; |
| break; |
| case 'a': |
| buf.append('&'); |
| bidx = p + 2; |
| break; |
| case '{': |
| buf.append('{'); |
| bidx = p + 2; |
| break; |
| case '/': // JSON have this |
| buf.append('/'); |
| bidx = p + 2; |
| break; |
| case 'x': |
| case 'u': |
| { |
| p += 2; |
| int x = p; |
| int y = 0; |
| int z = (ln - p) > 4 ? p + 4 : ln; |
| while (p < z) { |
| char c2 = tx.charAt(p); |
| if (c2 >= '0' && c2 <= '9') { |
| y <<= 4; |
| y += c2 - '0'; |
| } else if (c2 >= 'a' && c2 <= 'f') { |
| y <<= 4; |
| y += c2 - 'a' + 10; |
| } else if (c2 >= 'A' && c2 <= 'F') { |
| y <<= 4; |
| y += c2 - 'A' + 10; |
| } else { |
| break; |
| } |
| p++; |
| } |
| if (x < p) { |
| buf.append((char) y); |
| } else { |
| throw newSyntaxError( |
| "Invalid hexadecimal UNICODE escape in " |
| + "the string literal.", |
| x - 2); |
| } |
| bidx = p; |
| break; |
| } |
| default: |
| if (isWS(c)) { |
| boolean hasWS = false; |
| bidx = p + 1; |
| do { |
| if (c == 0xA || c == 0xD) { |
| if (hasWS) { |
| break; |
| } |
| hasWS = true; |
| if (c == 0xD && bidx < ln - 1) { |
| if (tx.charAt(bidx + 1) == 0xA) { |
| bidx++; |
| } |
| } |
| } |
| bidx++; |
| if (bidx == ln) { |
| break; |
| } |
| c = tx.charAt(bidx); |
| } while (isWS(c)); |
| if (!hasWS) { |
| throw newSyntaxError( |
| "Invalid usage of escape sequence " |
| + "\\white-space. This escape sequence " |
| + "can be used only before " |
| + "line-break."); |
| } |
| } else { |
| throw newSyntaxError( |
| "Invalid escape sequence \\" + c |
| + " in the string literal."); |
| } |
| } |
| p = bidx; |
| while (true) { |
| if (p == ln) { |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } |
| c = tx.charAt(p); |
| if (c == '\\') { |
| break; |
| } |
| if (c == q) { |
| buf.append(tx.substring(bidx, p)); |
| p++; |
| return buf.toString(); //! |
| } |
| p++; |
| } |
| } // while true |
| } // if quoted string |
| |
| // Raw string: |
| char c2; |
| if (p < ln - 1) { |
| c2 = tx.charAt(p + 1); |
| } else { |
| c2 = 0x20; |
| } |
| if (c == 'r' && (c2 == '"' || c2 == '\'')) { |
| char q = c2; |
| p += 2; |
| while (p < ln) { |
| c = tx.charAt(p); |
| p++; |
| if (c == q) { |
| return tx.substring(b + 2, p - 1); //! |
| } |
| } |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } |
| |
| // Unquoted string, boolean or number, or function call |
| uqsLoop: while (true) { |
| c = tx.charAt(p); |
| if (!isUnquotedStringChar(c) && !(p == b && c == '+')) { |
| break uqsLoop; |
| } |
| p++; |
| if (p == ln) { |
| break uqsLoop; |
| } |
| } |
| if (b == p) { |
| throw newSyntaxError("Unexpected character.", b); |
| } else { |
| String s = tx.substring(b, p); |
| int funcP = b; |
| int oldP = p; |
| c = skipWS(); |
| if (c == '(') { |
| p++; |
| List<Object> params; |
| boolean done = false; |
| try { |
| try { |
| ee.notify( |
| EvaluationEvent.ENTER_FUNCTION_PARAMS, |
| this, s, null); |
| } catch (Throwable e) { |
| throw newWrappedError(e, funcP); |
| } |
| done = true; |
| params = fetchListInner( |
| new ArrayList<Object>(), ')', forceStr); |
| } finally { |
| if (done) { |
| try { |
| ee.notify( |
| EvaluationEvent.LEAVE_FUNCTION_PARAMS, |
| this, s, null); |
| } catch (Throwable e) { |
| throw newWrappedError(e); |
| } |
| } |
| } |
| p++; |
| FunctionCall func = new FunctionCall(s, params); |
| if (!mapKey) { |
| try { |
| return ee.evalFunctionCall(func, this); //! |
| } catch (Throwable e) { |
| throw newError("Failed to evaluate function " |
| + TextUtil.jQuote(func.getName()) + ".", |
| b, e); |
| } |
| } else { |
| return func; |
| } |
| } else { |
| p = oldP; |
| if (!forceStr && !mapKey) { |
| if (s.equals("true")) { |
| return Boolean.TRUE; //! |
| } else if (s.equals("false")) { |
| return Boolean.FALSE; //! |
| } |
| c = s.charAt(0); |
| if ((c >= '0' && c <= '9') || c == '+' || c == '-') { |
| String s2; |
| if (c == '+') { |
| s2 = s.substring(1); // Integer(s) doesn't know +. |
| } else { |
| s2 = s; |
| } |
| try { |
| return new Integer(s2); //! |
| } catch (NumberFormatException exc) { |
| // ignore |
| } |
| try { |
| return new BigDecimal(s2); //! |
| } catch (NumberFormatException exc) { |
| // ignore |
| } |
| } |
| } |
| return s; //! |
| } // if not '(' |
| } // if b == p |
| } |
| |
| /** |
| * Skips a single expression. It's ignores syntax errors in the skipped |
| * expression as far as it is clean where the end of the expression is. |
| */ |
| private void skipExpression() throws EvaluationException { |
| char c; |
| |
| if (p >= ln) { //!!a |
| throw new BugException("Calling fetchExpression when p >= ln."); |
| } |
| |
| c = tx.charAt(p); |
| |
| // Map: |
| if (c == '{') { |
| p++; |
| skipListing('}'); |
| p++; |
| return; |
| } |
| |
| // List: |
| if (c == '[') { |
| p++; |
| skipListing(']'); |
| p++; |
| return; |
| } |
| |
| // Unresolved object in a dump: |
| if (c == '<') { |
| p++; |
| skipListing('>'); |
| p++; |
| return; |
| } |
| |
| // Just for durability: |
| if (c == '(') { |
| p++; |
| skipListing(')'); |
| p++; |
| return; |
| } |
| |
| int b = p; |
| |
| // Quoted string: |
| if (c == '"' || c == '\'') { |
| char q = c; |
| |
| p++; |
| while (p < ln) { |
| c = tx.charAt(p); |
| if (c == '\\') { |
| if (p != ln - 1) { |
| p++; |
| } |
| } |
| p++; |
| if (c == q) { |
| return; //! |
| } |
| } |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } // if quoted string |
| |
| // Raw string: |
| char c2; |
| if (p < ln - 1) { |
| c2 = tx.charAt(p + 1); |
| } else { |
| c2 = 0x20; |
| } |
| if (c == 'r' && (c2 == '"' || c2 == '\'')) { |
| char q = c2; |
| p += 2; |
| while (p < ln) { |
| c = tx.charAt(p); |
| p++; |
| if (c == q) { |
| return; //! |
| } |
| } |
| throw newSyntaxError( |
| "The closing " + TextUtil.jQuoteOrName(q) |
| + " of the string is missing.", |
| b); |
| } |
| |
| // Unquoted string, boolean or number, or function call |
| uqsLoop: while (true) { |
| c = tx.charAt(p); |
| if (!isUnquotedStringChar(c) |
| && !(p == b && c == '+')) { |
| break uqsLoop; |
| } |
| p++; |
| if (p == ln) { |
| break uqsLoop; |
| } |
| } |
| if (b == p) { |
| throw newSyntaxError("Unexpected character.", b); |
| } else { |
| int oldP = p; |
| c = skipWS(); |
| if (c == '(') { |
| p++; |
| skipListing(')'); |
| p++; |
| } else { |
| p = oldP; |
| } // if not '(' |
| } // if b == p |
| } |
| |
| private void skipListing(char terminator) throws EvaluationException { |
| int listP = p - 1; |
| skipWS(); |
| if (terminator == 0x20) { |
| listP = p; |
| } |
| |
| while (true) { |
| char c; |
| if (p < ln) { |
| c = tx.charAt(p); |
| if (c == terminator) { |
| return; |
| } |
| } else { |
| if (terminator == 0x20) { |
| return; |
| } else { |
| throw newSyntaxError("Reached the end of the text, " |
| + "but the closing " |
| + TextUtil.jQuoteOrName(terminator) |
| + " is missing.", |
| listP); |
| } |
| } |
| if (c == ',' || c == ':' || c == ';' || c == '=') { |
| p++; |
| } else { |
| skipExpression(); |
| } |
| c = skipWS(); |
| if (c == terminator) { |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Fetches separator between whatever items. |
| * |
| * @return the separator, which is either or <code>','</code>, |
| * or <code>':'</code>, or <code>0x20</code> for EOS, or |
| * <code>terminator</code> for the terminator character. |
| * <code>','</code> means comma separation, or separation with implied comma |
| * (i.e. sparation with NL). |
| * |
| * <p><code>p</code> will point the first character of the item (or the |
| * terminator character) after the skipped separator, unless an exception |
| * aborts the execution of the method. |
| * |
| * @param terminator the character that terminates the sequence of |
| * separated items. Use 0x20 for EOS. |
| * @param commaBadReason if not <code>null</code>, comma will not be |
| * accepted as separator, and it is the reason why. |
| * @param colonBadReason if not <code>null</code>, colon will not be |
| * accepted as separator, and it is the reason why. |
| */ |
| private char skipSeparator( |
| char terminator, String commaBadReason, String colonBadReason) |
| throws EvaluationException { |
| int intialP = p; |
| char c = skipWS(); |
| boolean plusConverted = false; |
| if (c == '+') { |
| // deprecated the old map-union syntax |
| throw newSyntaxError( |
| "The + operator is not supported. (Hint: if you want to " |
| + "break a string into multiple lines, use a quoted string " |
| + "literal, finish the line with \\, then just continue " |
| + "the literal in the next line with optional " |
| + "indentation."); |
| } |
| if (c == ',' || c == ':') { |
| if (commaBadReason != null && c == ',') { |
| if (!plusConverted) { |
| throw newSyntaxError( |
| "Comma (,) shouldn't be used here. " |
| + commaBadReason); |
| } else { |
| throw newSyntaxError( |
| "Plus sign (+), which is treated as comma (,) " |
| + "in this case, shouldn't be used here. " |
| + commaBadReason); |
| } |
| } |
| if (colonBadReason != null && c == ':') { |
| throw newSyntaxError( |
| "Colon (:) shouldn't be used here. " + colonBadReason); |
| } |
| p++; |
| skipWS(); |
| return c; |
| } else if (c == terminator) { |
| return terminator; |
| } else if (c == ';') { |
| throw newSyntaxError( |
| "Semicolon (;) was unexpected here. If you want to " |
| + "separate items in a listing then use comma " |
| + "(,) instead."); |
| } else if (c == '=') { |
| throw newSyntaxError( |
| "Equals sign (=) was unexpected here. If you want to " |
| + "associate a key with a value then use " |
| + "colon (:) instead."); |
| } else { |
| if (c == 0x20) { |
| // EOS |
| return c; |
| } |
| if (skipWSFoundNL) { |
| // implicit comma |
| if (commaBadReason != null) { |
| throw newSyntaxError( |
| "Line-break shouldn't be used before this iteam as " |
| + "separator (which is the same as using comma). " |
| + commaBadReason); |
| } |
| return ','; |
| } else { |
| if (p == intialP) { |
| throw newSyntaxError("Character " |
| + TextUtil.jQuoteOrName(tx.charAt(p)) |
| + " shouldn't occur here."); |
| } else { |
| // WS* separator |
| throw newSyntaxError("No separator was used before " |
| + "the item. Items in listings should be " |
| + "separated with comma (,) or line-break. Keys " |
| + "and values in maps should be separated with " |
| + "colon (:)."); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Increments <code>p</code> until it finds non-WS character or EOS, also |
| * it transparently skips CJSON comments. |
| * @return the non-WS char that terminates the WS, or 0x20 if EOS reached. |
| */ |
| private char skipWS() throws EvaluationException { |
| char c; |
| skipWSFoundNL = false; |
| while (p < ln) { |
| c = tx.charAt(p); |
| if (!isWS(c)) { |
| if (c == '/' && p + 1 < ln && tx.charAt(p + 1) == '/') { |
| while (true) { |
| p++; |
| if (p == ln) { |
| return 0x20; //! |
| } |
| c = tx.charAt(p); |
| if (c == 0xA || c == 0xD) { |
| skipWSFoundNL = true; |
| break; //! |
| } |
| } |
| } else if (c == '/' && p + 1 < ln && tx.charAt(p + 1) == '*') { |
| int commentP = p; |
| p++; |
| while (true) { |
| p++; |
| if (p + 1 >= ln) { |
| throw newSyntaxError( |
| "Comment was not closed with \"*/\".", |
| commentP); |
| } |
| if (tx.charAt(p) == '*' && tx.charAt(p + 1) == '/') { |
| p++; |
| break; //! |
| } |
| } |
| } else { |
| return c; //! |
| } |
| } else if (c == 0xD || c == 0xA) { |
| skipWSFoundNL = true; |
| } |
| p++; |
| } |
| return 0x20; |
| } |
| |
| /** |
| * (Re)inits the evaluator object. |
| */ |
| private void init(String text, String fileName, EvaluationEnvironment ee) { |
| p = 0; |
| skipWSFoundNL = false; |
| tx = text; |
| ln = text.length(); |
| this.fileName = fileName; |
| this.ee = ee == null ? SIMPLE_EVALUATION_ENVIRONMENT : ee; |
| } |
| |
| /** |
| * (Re)inits the evaluator object. |
| */ |
| private void init(Fragment fr, EvaluationEnvironment ee) { |
| p = fr.getFragmentStart(); |
| skipWSFoundNL = false; |
| tx = fr.getText(); |
| ln = fr.getFragmentEnd(); |
| this.fileName = fr.getFileName(); |
| this.ee = ee == null ? SIMPLE_EVALUATION_ENVIRONMENT : ee; |
| } |
| |
| private static final String ENCODING_COMMENT_1 = "encoding"; |
| private static final String ENCODING_COMMENT_2 = "charset"; |
| |
| /** |
| * Same as <code>Character.isWhitespace</code>, but counts BOM as WS too. |
| */ |
| private static boolean isWS(char c) { |
| return Character.isWhitespace(c) || c == 0xFEFF; |
| } |
| |
| private static boolean isUnquotedStringChar(char c) { |
| return c < 128 ? UQSTR_CHARS[c] : Character.isLetterOrDigit(c); |
| } |
| |
| /** |
| * @return the name of the charset given in the comment, or {@code null} if |
| * there is no such comment. |
| */ |
| private static String extractCharsetComment(byte[] b) { |
| char c; |
| String s; |
| int p = 0; |
| int ln = b.length; |
| |
| // Skip BOM, if present: |
| if (p + 2 < ln |
| && toChar(b[p]) == 0xEF |
| && toChar(b[p + 1]) == 0xBB |
| && toChar(b[p + 2]) == 0xBF) { |
| p += 3; |
| } |
| |
| // Skip WS |
| while (p < ln && Character.isWhitespace(toChar(b[p]))) { |
| p++; |
| } |
| |
| // Do we start with "//"? |
| if (!(p + 1 < ln && toChar(b[p]) == '/' && toChar(b[p + 1]) == '/')) { |
| return null; // No. |
| } |
| p += 2; |
| |
| p = extractCharsetComment_skipNonNLWS(b, p); |
| int bp = p; |
| while (p < ln) { |
| c = toChar(b[p]); |
| if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { |
| break; |
| } |
| p++; |
| } |
| if (p - bp != ENCODING_COMMENT_1.length() |
| && p - bp != ENCODING_COMMENT_2.length()) { |
| return null; |
| } |
| try { |
| s = new String(b, bp, p - bp, "ISO-8859-1").toLowerCase(); |
| } catch (UnsupportedEncodingException e) { |
| throw new BugException("ISO-8859-1 decoding failed.", e); |
| } |
| if (!s.equals(ENCODING_COMMENT_1) && !s.equals(ENCODING_COMMENT_2)) { |
| return null; |
| } |
| p = extractCharsetComment_skipNonNLWS(b, p); |
| if (p == ln) { |
| return null; |
| } |
| c = toChar(b[p]); |
| if (c != ':') { |
| return null; |
| } |
| p++; |
| p = extractCharsetComment_skipNonNLWS(b, p); |
| if (p == ln) { |
| return null; |
| } |
| bp = p; |
| while (p < ln) { |
| c = toChar(b[p]); |
| if (c == 0xA || c == 0xD) { |
| break; |
| } |
| p++; |
| } |
| try { |
| s = new String(b, bp, p - bp, "ISO-8859-1").trim(); |
| if (s.length() == 0) { |
| return null; |
| } |
| } catch (UnsupportedEncodingException e) { |
| throw new BugException("ISO-8859-1 decoding failed.", e); |
| } |
| return s; |
| } |
| |
| private static int extractCharsetComment_skipNonNLWS(byte[] b, int p) { |
| int ln = b.length; |
| while (p < ln) { |
| char c = toChar(b[p]); |
| if (!Character.isWhitespace(c) || c == 0xD || c == 0xA) { |
| break; |
| } |
| p++; |
| } |
| return p; |
| } |
| |
| private static char toChar(byte b) { |
| return (char) (0xFF & b); |
| } |
| |
| private static void dumpMap( |
| StringBuilder out, Map<String, Object> m, String indent) { |
| Iterator<Map.Entry<String, Object>> it = m.entrySet().iterator(); |
| while (it.hasNext()) { |
| Map.Entry<String, Object> ent = it.next(); |
| out.append( |
| indent |
| + TextUtil.jQuote(ent.getKey()) + ": "); |
| dumpValue(out, ent.getValue(), indent); |
| out.append(LINE_BREAK); |
| } |
| } |
| |
| private static void dumpMapSL(StringBuilder out, Map<String, Object> m) { |
| Iterator<Map.Entry<String, Object>> it = m.entrySet().iterator(); |
| while (it.hasNext()) { |
| Map.Entry<String, Object> ent = it.next(); |
| out.append(TextUtil.jQuote(ent.getKey()) + ":"); |
| dumpValueSL(out, ent.getValue()); |
| if (it.hasNext()) { |
| out.append(", "); |
| } |
| } |
| } |
| |
| private static void dumpList( |
| StringBuilder out, List<?> ls, String indent) { |
| Iterator<?> it = ls.iterator(); |
| while (it.hasNext()) { |
| Object obj = it.next(); |
| out.append(indent); |
| dumpValue(out, obj, indent); |
| out.append(LINE_BREAK); |
| } |
| } |
| |
| private static void dumpListSL(StringBuilder out, List<?> ls) { |
| Iterator<?> it = ls.iterator(); |
| while (it.hasNext()) { |
| Object obj = it.next(); |
| dumpValueSL(out, obj); |
| if (it.hasNext()) { |
| out.append(", "); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static void dumpValue(StringBuilder out, Object o, String indent) { |
| if (o instanceof Number || o instanceof Boolean) { |
| out.append(o); |
| } else if (o instanceof String) { |
| out.append(TextUtil.jQuote((String) o)); |
| } else if (o instanceof Map) { |
| out.append("{"); |
| out.append(LINE_BREAK); |
| dumpMap(out, (Map<String, Object>) o, indent + " "); |
| out.append(indent + "}"); |
| } else if (o instanceof List) { |
| out.append("["); |
| out.append(LINE_BREAK); |
| dumpList(out, (List<Object>) o, indent + " "); |
| out.append(indent + "]"); |
| } else if (o instanceof FunctionCall) { |
| FunctionCall dir = (FunctionCall) o; |
| out.append(dir.getName()); |
| out.append("("); |
| dumpListSL(out, dir.getParams()); |
| out.append(")"); |
| } else { |
| if (o == null) { |
| out.append("<null>"); |
| } else { |
| out.append("<"); |
| out.append(o.getClass().getName()); |
| out.append(" "); |
| out.append(TextUtil.jQuote(o.toString())); |
| out.append(">"); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static void dumpValueSL(StringBuilder out, Object o) { |
| if (o instanceof Number || o instanceof Boolean) { |
| out.append(o); |
| } else if (o instanceof String) { |
| out.append(TextUtil.jQuote((String) o)); |
| } else if (o instanceof Map) { |
| out.append("{"); |
| dumpMapSL(out, (Map<String, Object>) o); |
| out.append("}"); |
| } else if (o instanceof List) { |
| out.append("["); |
| dumpListSL(out, (List<Object>) o); |
| out.append("]"); |
| } else if (o instanceof FunctionCall) { |
| FunctionCall dir = (FunctionCall) o; |
| out.append(dir.getName()); |
| out.append("("); |
| dumpListSL(out, dir.getParams()); |
| out.append(")"); |
| } else { |
| out.append("<"); |
| out.append(o.getClass().getName()); |
| out.append(" "); |
| out.append(TextUtil.jQuote(o.toString())); |
| out.append(">"); |
| } |
| } |
| |
| private EvaluationException newSyntaxError(String message) { |
| return newSyntaxError(message, p); |
| } |
| |
| private EvaluationException newSyntaxError(String message, int position) { |
| return new EvaluationException( |
| "CJSON syntax error: " + message, tx, position, fileName); |
| } |
| |
| private EvaluationException newError(String message, int position) { |
| return new EvaluationException( |
| "CJSON error: " + message, tx, position, fileName); |
| } |
| |
| private EvaluationException newError( |
| String message, int position, Throwable cause) { |
| return new EvaluationException( |
| "CJSON error: " + message, tx, position, fileName, cause); |
| } |
| |
| private EvaluationException newWrappedError(Throwable e) { |
| return newWrappedError(e, p); |
| } |
| |
| private EvaluationException newWrappedError(Throwable e, int p) { |
| if (e instanceof EvaluationException) { |
| return (EvaluationException) e; |
| } |
| return new EvaluationException( |
| "Error while evaluating CJSON: " + e.getMessage(), |
| tx, p, fileName, e.getCause()); |
| } |
| |
| /** |
| * Symbolizes a CJSON function call. |
| * Function calls that are not evaluated during the evaluation of a CJSON |
| * expressions will be present in the result as the instances of this class. |
| */ |
| public static class FunctionCall { |
| private final String name; |
| private final List<Object> params; |
| |
| public FunctionCall(String name, List<Object> params) { |
| this.name = name; |
| this.params = params; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| |
| public List<Object> getParams() { |
| return params; |
| } |
| |
| @Override |
| public String toString() { |
| return CJSONInterpreter.dump(this); |
| } |
| } |
| |
| /** |
| * Fragment extracted from a CJSON expression. |
| */ |
| public static class Fragment { |
| private final String text; |
| private final int fragmentStart; |
| private final int fragmentEnd; |
| private final String fileName; |
| |
| /** |
| * Creates new CJSON fragment. |
| * |
| * @param text the full CJSON text that contains the fragment. |
| * (In extreme case the fragment and the full text is the same.) |
| * @param fragmentStart the start index of the fragment in the text. |
| * @param fragmentEnd the start index of the fragment in the text |
| * @param fileName the name of the file the text comes from (for |
| * informational purposes only). It can be <code>null</code> if the |
| * source file is unknown or there is no source file. |
| */ |
| public Fragment( |
| String text, int fragmentStart, int fragmentEnd, |
| String fileName) { |
| this.text = text; |
| this.fragmentStart = fragmentStart; |
| this.fragmentEnd = fragmentEnd; |
| this.fileName = fileName; |
| } |
| |
| /** |
| * Returns the name of the file the text comes from (for informational |
| * purposes only). It can be <code>null</code> if the source file is |
| * unknown or there is no source file. |
| */ |
| public String getFileName() { |
| return fileName; |
| } |
| |
| /** |
| * Returns the full CJSON text that contains the fragmet. |
| */ |
| public String getText() { |
| return text; |
| } |
| |
| /** |
| * Returns the start index of the fragment in the text. |
| */ |
| public int getFragmentStart() { |
| return fragmentStart; |
| } |
| |
| /** |
| * Returns the end index (exclusive) of the fragment in the text. |
| */ |
| public int getFragmentEnd() { |
| return fragmentEnd; |
| } |
| |
| /** |
| * Returns the fragment text. |
| */ |
| @Override |
| public String toString() { |
| return text.substring(fragmentStart, fragmentEnd); |
| } |
| } |
| |
| public enum EvaluationEvent { |
| /** |
| * The code of event that indicates that we have started to evaluate the |
| * value in a key:value pair. |
| */ |
| ENTER_MAP_KEY, |
| |
| /** |
| * The code of event that indicates that we have finished to evaluate |
| * the value in a key:value pair. |
| */ |
| LEAVE_MAP_KEY, |
| |
| /** |
| * The code of event that indicates that we have started to evaluate the |
| * parameter list in a function call. |
| */ |
| ENTER_FUNCTION_PARAMS, |
| |
| /** |
| * The code of event that indicates that we have finished to evaluate |
| * the parameter list in a function call. |
| */ |
| LEAVE_FUNCTION_PARAMS, |
| |
| /** |
| * The code of event that indicates that we have started to evaluate the |
| * items in a list. This does not include function call parameter lists. |
| */ |
| ENTER_LIST, |
| |
| /** |
| * The code of event that indicates that we have finished to evaluate |
| * the items in a list. |
| */ |
| LEAVE_LIST, |
| |
| |
| /** |
| * The code of event that indicates that we have started to evaluate the |
| * items in a map. |
| */ |
| ENTER_MAP, |
| |
| /** |
| * The code of event that indicates that we have finished to evaluate |
| * the items in a list. |
| */ |
| LEAVE_MAP |
| } |
| |
| /** |
| * Callbacks that let you control the behavior of CJSON expression |
| * evaluation. |
| */ |
| public interface EvaluationEnvironment { |
| |
| Object RETURN_SKIP = new Object(); |
| |
| Object RETURN_FRAGMENT = new Object(); |
| |
| /** |
| * Evaluates the function call. This method may simply returns its |
| * parameter, which means that the function was not resolved, and thus |
| * the function call will be available for further interpretation in the |
| * result of the CJSON expression evaluation. |
| * |
| * @param fc the function call to evaluate. |
| * |
| * @return the return value of the function call. During the evaluation |
| * of a CJSON expression, function calls will be replaced with |
| * their return values. |
| * If the return value is a {@link FunctionCall} object, it will not |
| * be evaluated again. This way, the final result of a CJSON |
| * expression evaluation can contain {@link FunctionCall} objects. |
| * @throws Exception |
| */ |
| Object evalFunctionCall(FunctionCall fc, CJSONInterpreter ip) |
| throws Exception; |
| |
| /** |
| * Notifies about an event during expression evaluation. |
| * |
| * @param event An <code>EVENT_...</code> constant. Further events may |
| * will be added later, so the implementation must silently ignore |
| * events that it does not know. It is guaranteed that for each |
| * <code>EVENT_ENTER_...</code> event there will be an |
| * <code>EVENT_LEAVE_...</code> event later, except if |
| * <code>notifyContextChange</code> has thrown exception during |
| * handling <code>EVENT_ENTER_...</code>, in which case it is |
| * guaranteed that there will be no corresponding |
| * <code>EVENT_LEAVE_...</code> event. |
| * @param ip the {@link CJSONInterpreter} instance that evaluates the |
| * text. The value returned by |
| * {@link CJSONInterpreter#getPosition()} will be the position in |
| * the text where the this even has been created: |
| * <ul> |
| * <li>{@link EvaluationEvent#ENTER_MAP_KEY}: points the first |
| * character of the <i>value</i> of the key:<i>value</i> |
| * pair. |
| * <li>{@link EvaluationEvent#ENTER_LIST}, |
| * {@link EvaluationEvent#ENTER_MAP}, and |
| * {@link EvaluationEvent#ENTER_FUNCTION_PARAMS}: points the |
| * first character after the <tt>[</tt> and <tt>(</tt> |
| * respectively. |
| * <li>{@link EvaluationEvent#LEAVE_LIST}, |
| * {@link EvaluationEvent#LEAVE_MAP}, and |
| * {@link EvaluationEvent#LEAVE_FUNCTION_PARAMS}: points the |
| * terminating character, that is, the <tt>]</tt> or |
| * <tt>)</tt> or the character after the end of the string. |
| * </ul> |
| * @param name For {@link EvaluationEvent#ENTER_MAP_KEY} and |
| * {@link EvaluationEvent#ENTER_FUNCTION_PARAMS}, the name of the |
| * map key or function. It is <code>null</code> otherwise. |
| * @param extra Even specific extra information. |
| * <ul> |
| * <li>For {@link EvaluationEvent#ENTER_MAP}, |
| * {@link EvaluationEvent#LEAVE_MAP}, |
| * {@link EvaluationEvent#ENTER_LIST}, |
| * {@link EvaluationEvent#LEAVE_LIST} it is the |
| * <code>Map</code> or <code>List</code> that is being |
| * built by the map or list. It's OK to modify this |
| * <code>Map</code> or <code>List</code>. |
| * <li>For other events it's |
| * value is currently <code>null</code>. |
| * </ul> |
| * @return return The allowed return values and their meaning depends on |
| * the event. But return value <code>null</code> always means |
| * "do nothing special". The currently defined non-<code>null</code> |
| * return values for the events: |
| * <ul> |
| * <li>{@link EvaluationEvent#ENTER_MAP_KEY}: |
| * <ul> |
| * <li>{@link #RETURN_SKIP}: Skip the key:value |
| * pair. That is, the key:value pair will not be added to |
| * the map. The value expression will not be evaluated. |
| * <li>{@link #RETURN_FRAGMENT}: The value of the key:value |
| * pair will be the {@link Fragment} that stores the |
| * value expression. The value expression will not be |
| * evaluated. |
| * However, if the value is implicit boolean |
| * <code>true</code>, (i.e. you omit the value) then |
| * {@link #RETURN_FRAGMENT} has no effect. |
| * </ul> |
| * <li> |
| * <li>{@link EvaluationEvent#ENTER_MAP} if the map uses |
| * <tt>{</tt> and <tt>}</tt>): |
| * <ul> |
| * <li>{@link #RETURN_FRAGMENT}: The value of the map will be |
| * the {@link Fragment} that stores the map expression. |
| * The map expression will not be evaluated. |
| * </ul> |
| * </li> |
| * </ul> |
| */ |
| Object notify( |
| EvaluationEvent event, CJSONInterpreter ip, |
| String name, Object extra) |
| throws Exception; |
| } |
| |
| public static class EvaluationException extends Exception { |
| public EvaluationException(String message) { |
| super(message); |
| } |
| |
| public EvaluationException(String message, Throwable cause) { |
| super(message, cause); |
| } |
| |
| public EvaluationException(String message, int position) { |
| super(message + LINE_BREAK |
| + "Error location: character " + (position + 1)); |
| } |
| |
| public EvaluationException( |
| String message, int position, Throwable cause) { |
| super(message + LINE_BREAK |
| + "Error location: character " + (position + 1), |
| cause); |
| } |
| |
| public EvaluationException( |
| String message, String text, int position, String fileName) { |
| super(createSourceCodeErrorMessage( |
| message, text, position, fileName, 56)); |
| } |
| |
| public EvaluationException( |
| String message, String text, int position, String fileName, |
| Throwable cause) { |
| super(createSourceCodeErrorMessage( |
| message, text, position, fileName, 56), |
| cause); |
| } |
| } |
| |
| private static final String LINE_BREAK = "\n"; |
| |
| private static String createSourceCodeErrorMessage( |
| String message, String srcCode, int position, String fileName, |
| int maxQuotLength) { |
| int ln = srcCode.length(); |
| if (position < 0) { |
| position = 0; |
| } |
| if (position >= ln) { |
| if (position == ln) { |
| return message + LINE_BREAK |
| + "Error location: The very end of " |
| + (fileName == null ? "the text" : fileName) |
| + "."; |
| } else { |
| return message + LINE_BREAK |
| + "Error location: ??? (after the end of " |
| + (fileName == null ? "the text" : fileName) |
| + ")"; |
| } |
| } |
| |
| int i; |
| char c; |
| int rowBegin = 0; |
| int rowEnd; |
| int row = 1; |
| char lastChar = 0; |
| for (i = 0; i <= position; i++) { |
| c = srcCode.charAt(i); |
| if (lastChar == 0xA) { |
| rowBegin = i; |
| row++; |
| } else if (lastChar == 0xD && c != 0xA) { |
| rowBegin = i; |
| row++; |
| } |
| lastChar = c; |
| } |
| for (i = position; i < ln; i++) { |
| c = srcCode.charAt(i); |
| if (c == 0xA || c == 0xD) { |
| if (c == 0xA && i > 0 && srcCode.charAt(i - 1) == 0xD) { |
| i--; |
| } |
| break; |
| } |
| } |
| rowEnd = i - 1; |
| if (position > rowEnd + 1) { |
| position = rowEnd + 1; |
| } |
| int col = position - rowBegin + 1; |
| if (rowBegin > rowEnd) { |
| return message + LINE_BREAK |
| + "Error location: line " |
| + row + ", column " + col |
| + (fileName == null ? ":" : " in " + fileName + ":") |
| + LINE_BREAK |
| + "(Can't show the line because it is empty.)"; |
| } |
| String s1 = srcCode.substring(rowBegin, position); |
| String s2 = srcCode.substring(position, rowEnd + 1); |
| s1 = expandTabs(s1, 8); |
| int ln1 = s1.length(); |
| s2 = expandTabs(s2, 8, ln1); |
| int ln2 = s2.length(); |
| if (ln1 + ln2 > maxQuotLength) { |
| int newLn2 = ln2 - ((ln1 + ln2) - maxQuotLength); |
| if (newLn2 < 6) { |
| newLn2 = 6; |
| } |
| if (newLn2 < ln2) { |
| s2 = s2.substring(0, newLn2 - 3) + "..."; |
| ln2 = newLn2; |
| } |
| if (ln1 + ln2 > maxQuotLength) { |
| s1 = "..." + s1.substring((ln1 + ln2) - maxQuotLength + 3); |
| } |
| } |
| StringBuilder res = new StringBuilder(message.length() + 80); |
| res.append(message); |
| res.append(LINE_BREAK); |
| res.append("Error location: line "); |
| res.append(row); |
| res.append(", column "); |
| res.append(col); |
| if (fileName != null) { |
| res.append(" in "); |
| res.append(fileName); |
| } |
| res.append(":"); |
| res.append(LINE_BREAK); |
| res.append(s1); |
| res.append(s2); |
| res.append(LINE_BREAK); |
| int x = s1.length(); |
| while (x != 0) { |
| res.append(' '); |
| x--; |
| } |
| res.append('^'); |
| |
| return res.toString(); |
| } |
| |
| /** |
| * Same as <code>expandTabs(text, tabWidth, 0)</code>. |
| * @see #expandTabs(String, int, int) |
| */ |
| private static String expandTabs(String text, int tabWidth) { |
| return expandTabs(text, tabWidth, 0); |
| } |
| |
| /** |
| * Replaces all occurances of character tab with spaces. |
| * @param tabWidth the distance of tab stops. |
| * @param startCol the index of the column in which the first character of |
| * the string is from the left edge of the page. The index of the first |
| * column is 0. |
| * @return String The string after the replacements. |
| */ |
| private static String expandTabs(String text, int tabWidth, int startCol) { |
| int e = text.indexOf('\t'); |
| if (e == -1) { |
| return text; |
| } |
| int b = 0; |
| int tln = text.length(); |
| StringBuilder buf = new StringBuilder(tln + 16); |
| do { |
| buf.append(text.substring(b, e)); |
| int col = buf.length() + startCol; |
| for (int i = tabWidth * (1 + col / tabWidth) - col; i > 0; i--) { |
| buf.append(' '); |
| } |
| b = e + 1; |
| e = text.indexOf('\t', b); |
| } while (e != -1); |
| buf.append(text.substring(b)); |
| return buf.toString(); |
| } |
| |
| private static byte[] loadByteArray(InputStream in) |
| throws IOException { |
| return loadByteArray(in, 512, false, 2); |
| } |
| |
| private static byte[] loadByteArray( |
| InputStream in, int initialSize, boolean sizeExpected, |
| double multipier) |
| throws IOException { |
| int size = 0; |
| int bcap = initialSize; |
| byte[] b = new byte[bcap]; |
| try { |
| int rdn; |
| readLoop: while ((rdn = in.read(b, size, bcap - size)) != -1) { |
| size += rdn; |
| if (bcap == size) { |
| int nextByte = -1; |
| if (sizeExpected) { |
| // If the initialSize was the expected size of the |
| // "file", then resizing the buffer is certainly |
| // needless, as the next in.read(...) call would just |
| // return with -1. So let's see if it would... |
| nextByte = in.read(); |
| if (nextByte == -1) { |
| break readLoop; |
| } |
| } |
| |
| bcap = (int) (bcap * multipier) + 64; |
| byte[] newB = new byte[bcap]; |
| System.arraycopy(b, 0, newB, 0, size); |
| b = newB; |
| |
| // We have guessed badly, so... |
| if (nextByte != -1) { |
| b[size] = (byte) nextByte; |
| size++; |
| } |
| } |
| } |
| } finally { |
| in.close(); |
| } |
| if (b.length != size) { |
| byte[] newB = new byte[size]; |
| System.arraycopy(b, 0, newB, 0, size); |
| return newB; |
| } else { |
| return b; |
| } |
| } |
| |
| } |