1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  *
   4  * Redistribution and use in source and binary forms, with or without
   5  * modification, are permitted provided that the following conditions
   6  * are met:
   7  *
   8  *   - Redistributions of source code must retain the above copyright
   9  *     notice, this list of conditions and the following disclaimer.
  10  *
  11  *   - Redistributions in binary form must reproduce the above copyright
  12  *     notice, this list of conditions and the following disclaimer in the
  13  *     documentation and/or other materials provided with the distribution.
  14  *
  15  *   - Neither the name of Oracle nor the names of its
  16  *     contributors may be used to endorse or promote products derived
  17  *     from this software without specific prior written permission.
  18  *
  19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  20  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  21  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  22  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  26  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  27  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  28  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  29  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30  */
  31 
  32 /*
  33  * Hjson - "the Human JSON - A configuration file format that 
  34  * caters to humans and helps reduce the errors they make"
  35  * See also: http://hjson.org/
  36  *
  37  * I wanted to see if we can use Nashorn Parser API (jdk9) to support
  38  * similar flexible JSON extension with #nashorn. In this FlexiJSON.parse
  39  * implementation, Nashorn Parser API is used to validate that the 
  40  * extendable flexi JSON is "data only" (i.e., no executable code) and
  41  * then 'eval'ed to make an object out of it.
  42  *
  43  * FlexiJSON allows the following:
  44  *
  45  *   * single and mutliple line comments anywhere
  46  *   * non-quoted property names and values
  47  *   * regexp literal values
  48  *   * omitting trailing comma
  49  *
  50  * When nashorn -scripting mode is enabled, FlexiJSON supports these
  51  * as well:
  52  *
  53  *   * shell style # comments
  54  *   * multiple line (Unix heredoc style) string values
  55  */
  56 
  57 "use strict";
  58 
  59 function FlexiJSON() {}
  60 
  61 // helper to locate Nashorn Parser API classes
  62 FlexiJSON.treeType = function(name) {
  63     return Java.type("jdk.nashorn.api.tree." + name);
  64 }
  65 
  66 // Nashorn Parser API classes used
  67 FlexiJSON.ArrayLiteral = FlexiJSON.treeType("ArrayLiteralTree");
  68 FlexiJSON.ExpressionStatement = FlexiJSON.treeType("ExpressionStatementTree");
  69 FlexiJSON.ObjectLiteral = FlexiJSON.treeType("ObjectLiteralTree");
  70 FlexiJSON.RegExpLiteral = FlexiJSON.treeType("RegExpLiteralTree");
  71 FlexiJSON.Literal = FlexiJSON.treeType("LiteralTree");
  72 FlexiJSON.Parser = FlexiJSON.treeType("Parser");
  73 FlexiJSON.SimpleTreeVisitor = FlexiJSON.treeType("SimpleTreeVisitorES5_1");
  74 
  75 // FlexiJSON.parse API
  76 
  77 FlexiJSON.parse = function(str) {
  78     var parser = (typeof $OPTIONS == "undefined")? 
  79         FlexiJSON.Parser.create() :
  80         FlexiJSON.Parser.create("-scripting");
  81 
  82     // force the string to be an expression by putting it inside (, )
  83     str = "(" + str + ")";
  84     var ast = parser.parse("<flexijsondoc>", str, null);
  85     // Should not happen. parse would have thrown syntax error
  86     if (!ast) {
  87         return undefined;
  88     }
  89 
  90     // allowed 'literal' values in flexi JSON
  91     function isLiteral(node) {
  92         return node instanceof FlexiJSON.ArrayLiteral ||
  93             node instanceof FlexiJSON.Literal ||
  94             node instanceof FlexiJSON.ObjectLiteral ||
  95             node instanceof FlexiJSON.RegExpLiteral;
  96     }
  97 
  98     var visitor;
  99     ast.accept(visitor = new (Java.extend(FlexiJSON.SimpleTreeVisitor)) {
 100          lineMap: null,
 101 
 102          throwError: function(msg, node) {
 103              if (this.lineMap) {
 104                  var pos = node.startPosition;
 105                  var line = this.lineMap.getLineNumber(pos);
 106                  var column = this.lineMap.getColumnNumber(pos);
 107                  // we introduced extra '(' at start. So, adjust column number
 108                  msg = msg + " @ " + line + ":" + (column - 1);
 109              }
 110              throw new TypeError(msg);
 111          },
 112 
 113          visitLiteral: function(node, extra) {
 114              print(node.value);
 115          },
 116 
 117          visitExpressionStatement: function(node, extra) {
 118              var expr = node.expression;
 119              if (isLiteral(expr)) {
 120                  expr.accept(visitor, extra);
 121              } else {
 122                  this.throwError("only literals can occur", expr);
 123              }
 124          },
 125 
 126          visitArrayLiteral: function(node, extra) {
 127              for each (var elem in node.elements) {
 128                  if (isLiteral(elem)) {
 129                      elem.accept(visitor, extra);
 130                  } else {
 131                      this.throwError("only literal array element value allowed", elem);
 132                  }
 133              }
 134          },
 135 
 136          visitObjectLiteral: function(node, extra) {
 137              for each (var prop in node.properties) {
 138                  if (prop.getter != null || prop.setter != null) {
 139                      this.throwError("getter/setter property not allowed", node);
 140                  }
 141 
 142                  var value = prop.value;
 143                  if (isLiteral(value)) {
 144                      value.accept(visitor, extra);
 145                  } else {
 146                      this.throwError("only literal property value allowed", value);
 147                  }
 148              }
 149          },
 150 
 151          visitCompilationUnit: function(node, extra) {
 152              this.lineMap = node.lineMap;
 153              var elements = node.sourceElements;
 154              if (elements.length > 1) {
 155                  this.throwError("more than one top level expression", node.sourceElements[1]);
 156              } 
 157              var stat = node.sourceElements[0];
 158              if (! (stat instanceof FlexiJSON.ExpressionStatement)) {
 159                  this.throwError("only one top level expresion allowed", stat);
 160              }
 161              stat.accept(visitor, extra);
 162          },
 163     }, null);
 164 
 165     // safe to eval given string as flexi JSON!
 166     return eval(str);
 167 }