001/* Converts a string containing JSON-formatted data to a Token. 002 003 Copyright (c) 2012-2016 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 027 */ 028 029package ptolemy.actor.lib.conversions.json; 030 031import java.util.ArrayList; 032import java.util.Iterator; 033import java.util.Set; 034 035import org.json.JSONArray; 036import org.json.JSONException; 037import org.json.JSONObject; 038 039import ptolemy.actor.lib.conversions.Converter; 040import ptolemy.data.ArrayToken; 041import ptolemy.data.BooleanToken; 042import ptolemy.data.DateToken; 043import ptolemy.data.DoubleToken; 044import ptolemy.data.IntToken; 045import ptolemy.data.LongToken; 046import ptolemy.data.ObjectToken; 047import ptolemy.data.RecordToken; 048import ptolemy.data.StringToken; 049import ptolemy.data.Token; 050import ptolemy.data.type.BaseType; 051import ptolemy.graph.Inequality; 052import ptolemy.kernel.CompositeEntity; 053import ptolemy.kernel.util.IllegalActionException; 054import ptolemy.kernel.util.NameDuplicationException; 055 056/** 057An actor that converts a string containing JSON-formatted data into a Token. 058 059<p>Depending on the top level structure found in the JSON string, it produces 060either a RecordToken or an ArrayToken on its output port. Nested structures 061in the JSON data will translate to correspondingly nested structures in the 062Token.</p> 063 064<p>The JSONObject parser processes values as follows: 065Delimited values are always parsed as a String. Values that are not delimited 066are tested in the order noted below. The first test that succeeds determines 067the type.</p> 068<ul> 069 <li>'true' | 'false' => Boolean (case insensitive)</li> 070 <li>'null' => JSONObject.NULL (case insensitive)</li> 071 <li>'0x..' => Integer (hexadecimal)</li> 072 <li>x'.'y | exponent encoded => Double</li> 073 <li>x => Long, or Integer if value remains the same after conversion</li> 074</ul> 075<p>If non of the above apply, the value is interpreted as a String.</p> 076<p>Note that JSON allows array elements to have different types, whereas the 077<code>ArrayToken</code> does not. Conversion of such mixed array will result 078in an <code>ArrayToken</code> of which the types of all elements are cast to 079the least upper bound of the entire collection.</p> 080 081<p><a href="http://www.json.org/">http://www.json.org/</a> 082- a description of the JSON format.</p> 083 084@see TokenToJSON 085@author Marten Lohstroh and Edward A. Lee, Contributor: Beth Latronico 086@version $Id$ 087@since Ptolemy II 10.0 088@Pt.ProposedRating Yellow (marten) 089@Pt.AcceptedRating Red (chx) 090 */ 091public class JSONToToken extends Converter { 092 093 /** Construct a JSONToToken actor with the given container and name. 094 * @param container The container. 095 * @param name The name of this actor. 096 * @exception IllegalActionException If the actor cannot be contained 097 * by the proposed container. 098 * @exception NameDuplicationException If the container already has an 099 * actor with this name. 100 */ 101 public JSONToToken(CompositeEntity container, String name) 102 throws NameDuplicationException, IllegalActionException { 103 super(container, name); 104 input.setTypeEquals(BaseType.STRING); 105 } 106 107 /////////////////////////////////////////////////////////////////// 108 //// public methods //// 109 110 /** Read a JSON-formatted String of name/value pairs from the input 111 * and produce a corresponding array or record on the output. 112 * @exception IllegalActionException If the input string does not 113 * contain properly formatted JSON. 114 */ 115 @Override 116 public void fire() throws IllegalActionException { 117 super.fire(); 118 Token token = parseJSON(((StringToken) input.get(0)).stringValue()); 119 if (token == null) { 120 throw new IllegalActionException(this, 121 "Unable to parse JSON data: " + input); 122 } 123 output.send(0, token); 124 } 125 126 /** 127 * Parse the input string and return a token representation of the data. 128 * A JSON object is converted to a RecordToken, a JSON array to an ArrayToken, 129 * a string to a StringToken, "true" and "false" to BooleanToken, and an empty 130 * string, "nil", or "null" to a nil token. 131 * @param input An input string that contains JSON-formatted data 132 * @return A Token that represents the JSON-formatted input string 133 * @exception IllegalActionException If the given input string cannot be parsed. 134 */ 135 public static Token parseJSON(String input) throws IllegalActionException { 136 try { 137 input = input.trim(); 138 if (input.length() == 0 || input.equals("nil") 139 || input.equals("null")) { 140 return Token.NIL; 141 } else if (input.startsWith("{") && input.endsWith("}")) { 142 return _scanJSONObject(new JSONObject(input)); 143 } else if (input.startsWith("[") && input.endsWith("]")) { 144 return _scanJSONArray(new JSONArray(input)); 145 } else if (input.startsWith("\"") && input.endsWith("\"")) { 146 return new StringToken(input.substring(1, input.length() - 1)); 147 } else if (input.startsWith("date(\"") && input.endsWith("\")")) { 148 return new DateToken(input.substring(6, input.length() - 2)); 149 } else if (input.equals("true")) { 150 return BooleanToken.TRUE; 151 } else if (input.equals("false")) { 152 return BooleanToken.FALSE; 153 } else { 154 // Last remaining possibility is a number. 155 try { 156 int result = Integer.parseInt(input); 157 return new IntToken(result); 158 } catch (NumberFormatException ex) { 159 try { 160 double result = Double.parseDouble(input); 161 return new DoubleToken(result); 162 } catch (NumberFormatException e) { 163 throw new IllegalActionException( 164 "Invalid JSON: " + input); 165 } 166 } 167 } 168 } catch (JSONException e) { 169 throw new IllegalActionException( 170 "Invalid JSON: " + input + "\n" + e.getMessage()); 171 } 172 } 173 174 /** Return false if the input port has no token, otherwise return 175 * what the superclass returns (presumably true). 176 * @exception IllegalActionException If there is no director. 177 */ 178 @Override 179 public boolean prefire() throws IllegalActionException { 180 if (!input.hasToken(0)) { 181 return false; 182 } 183 return super.prefire(); 184 } 185 186 /////////////////////////////////////////////////////////////////// 187 //// protected methods //// 188 189 /** 190 * Do not establish the usual default type constraints. 191 */ 192 @Override 193 protected Set<Inequality> _defaultTypeConstraints() { 194 return null; 195 } 196 197 /////////////////////////////////////////////////////////////////// 198 //// private methods //// 199 200 /** Map an given value to the appropriate Token class and return the 201 * new Token. 202 * @param value An Object representing some value 203 * @return A Token representing the given value 204 * @exception JSONException If a non-existent value is requested from the 205 * given object or array. 206 * @exception IllegalActionException Upon failing to instantiate a new 207 * Token. 208 */ 209 private static Token _mapValueToToken(Object value) 210 throws IllegalActionException, JSONException { 211 212 // The value can be any of these types: 213 // Boolean, Number, String, or the JSONObject.NULL 214 if (value instanceof JSONArray) { 215 return _scanJSONArray((JSONArray) value); 216 } else if (value instanceof JSONObject) { 217 return _scanJSONObject((JSONObject) value); 218 } else { 219 Token t; 220 if (value instanceof Boolean) { 221 t = new BooleanToken((Boolean) value); 222 } else if (value instanceof Integer) { 223 t = new IntToken((Integer) value); 224 } else if (value instanceof Long) { 225 t = new LongToken((Long) value); 226 } else if (value instanceof Double) { 227 t = new DoubleToken((Double) value); 228 } else if (value instanceof String) { 229 t = new StringToken((String) value); 230 } else if (value.equals(JSONObject.NULL)) { 231 t = new ObjectToken(null); 232 } else { 233 throw new IllegalActionException("Unable to map value of " 234 + value.getClass().toString() + " to token."); 235 } 236 return t; 237 238 } 239 240 } 241 242 /** Iterate over the elements inside a JSONArray and put them inside a 243 * new ArrayToken. Apply recursion for JSONObjects and JSONArrays. 244 * When a new ArrayToken is instantiated, all elements are converted to 245 * the least upper bound of the types found in the JSONArray. If the 246 * conversion fails, an IllegalActionException is thrown. 247 * @param array A JSONArray 248 * @return An ArrayToken containing the values that corresponding to those 249 * found in the given array 250 * @exception JSONException If a non-existent value is requested from the 251 * given array. 252 * @exception IllegalActionException Upon failing to instantiate a new 253 * ArrayToken. 254 */ 255 private static ArrayToken _scanJSONArray(JSONArray array) 256 throws JSONException, IllegalActionException { 257 ArrayList<Token> values = new ArrayList<Token>(); 258 259 Object value; 260 261 for (int i = 0; i < array.length(); ++i) { 262 value = array.get(i); 263 values.add(_mapValueToToken(value)); 264 } 265 266 // If there are no values, ArrayToken() requires a special constructor 267 if (values.isEmpty()) { 268 return new ArrayToken(BaseType.UNKNOWN); 269 } else { 270 return new ArrayToken(values.toArray(new Token[values.size()])); 271 } 272 } 273 274 /** Iterate over the elements inside a JSONObject and put them inside a 275 * new RecordToken. Apply recursion for JSONObjects and JSONArrays. 276 * 277 * @param object A JSONObject 278 * @return A RecordToken containing fields and values that correspond 279 * with those found in the given object 280 * @exception JSONException If a non-existent value is requested from the 281 * given object. 282 * @exception IllegalActionException Upon failing to instantiate a new 283 * RecordToken. 284 */ 285 private static RecordToken _scanJSONObject(JSONObject object) 286 throws IllegalActionException, JSONException { 287 ArrayList<String> names = new ArrayList<String>(); 288 ArrayList<Token> values = new ArrayList<Token>(); 289 290 Object value; 291 String name; 292 Iterator<?> i = object.keys(); 293 294 while (i.hasNext()) { 295 name = (String) i.next(); 296 value = object.get(name); 297 names.add(name); 298 values.add(_mapValueToToken(value)); 299 } 300 301 return new RecordToken(names.toArray(new String[names.size()]), 302 values.toArray(new Token[values.size()])); 303 } 304 305}