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' =&gt; Boolean (case insensitive)</li>
070  <li>'null' =&gt; JSONObject.NULL (case insensitive)</li>
071  <li>'0x..' =&gt; Integer (hexadecimal)</li>
072  <li>x'.'y | exponent encoded =&gt; Double</li>
073  <li>x =&gt; 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}