Commit 0baf0d63 authored by Evan W. Patton's avatar Evan W. Patton Committed by Jeffrey Schiller

Implement dictionary-based JSON and XML methods in Web component

Change-Id: Ie8ff8124d159d03d9e099e15f4c0a61a0355e893
parent f1e1863f
......@@ -1586,6 +1586,12 @@ public final class YoungAndroidFormUpgrader {
// timeout behavior.
srcCompVersion = 6;
}
if (srcCompVersion < 7) {
// The JsonTextDecodeWithDictionaries was added to parse JSON using dictionaries.
// The XMLTextDecodeAsDictionary was added to provide a more robust representation
// of XML using dictionaries.
srcCompVersion = 7;
}
return srcCompVersion;
}
......
......@@ -2628,7 +2628,10 @@ Blockly.Versioning.AllUpgradeMaps =
5: "noUpgrade",
// AI2: Added property Timeout and event TimedOut
6: "noUpgrade"
6: "noUpgrade",
// AI2: Added methods JsonTextDecodeWithDictionaries and XMLTextDecodeAsDictionary
7: "noUpgrade"
}, // End Web upgraders
......
......@@ -490,6 +490,7 @@ public class YaVersion {
// - WEBVIEWER_COMPONENT_VERSION was incremented to 8
// For YOUNG_ANDROID_VERSION 197:
// - BLOCKS_LANGUAGE_VERSION was incremented to 28
// - WEB_COMPONENT_VERSION was incremented to 7
public static final int YOUNG_ANDROID_VERSION = 197;
......@@ -1264,7 +1265,10 @@ public class YaVersion {
// For WEB_COMPONENT_VERSION 6:
// - The Timeout property was added.
// - The TimedOut event was added for timed out web requests.
public static final int WEB_COMPONENT_VERSION = 6;
// For WEB_COMPONENT_VERSION 7:
// - The JsonTextDecodeWithDictionaries method was added
// - The XMLTextDecodeAsDictionary method was added.
public static final int WEB_COMPONENT_VERSION = 7;
// For WEBVIEWER_COMPONENT_VERSION 2:
// - The CanGoForward and CanGoBack methods were added
......
......@@ -6,6 +6,10 @@
package com.google.appinventor.components.runtime;
import android.app.Activity;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
......@@ -24,6 +28,7 @@ import com.google.appinventor.components.runtime.collect.Maps;
import com.google.appinventor.components.runtime.errors.IllegalArgumentError;
import com.google.appinventor.components.runtime.errors.PermissionException;
import com.google.appinventor.components.runtime.errors.RequestTimeoutException;
import com.google.appinventor.components.runtime.repackaged.org.json.XML;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.FileUtil;
......@@ -31,16 +36,15 @@ import com.google.appinventor.components.runtime.util.GingerbreadUtil;
import com.google.appinventor.components.runtime.util.JsonUtil;
import com.google.appinventor.components.runtime.util.MediaUtil;
import com.google.appinventor.components.runtime.util.SdkLevel;
import com.google.appinventor.components.runtime.util.XmlParser;
import com.google.appinventor.components.runtime.util.YailDictionary;
import com.google.appinventor.components.runtime.util.YailList;
import android.app.Activity;
import android.text.TextUtils;
import android.util.Log;
import com.google.appinventor.components.runtime.util.YailObject;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.appinventor.components.runtime.repackaged.org.json.XML;
import org.xml.sax.InputSource;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
......@@ -48,6 +52,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
......@@ -56,8 +61,8 @@ import java.net.ProtocolException;
import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
......@@ -852,6 +857,9 @@ public class Web extends AndroidNonvisibleComponent implements Component {
* (denoted as `{A:B}`) decodes to a list `((A B))`, that is, a list containing the two-element
* list `(A B)`.
*
* Use the method [JsonTextDecodeWithDictionaries](#Web.JsonTextDecodeWithDictionaries) if you
* would prefer to get back dictionary objects rather than lists-of-lists in the result.
*
* @param jsonText the JSON text to decode
* @return the decoded text
*/
......@@ -866,7 +874,7 @@ public class Web extends AndroidNonvisibleComponent implements Component {
// dictionaries
public Object JsonTextDecode(String jsonText) {
try {
return decodeJsonText(jsonText);
return decodeJsonText(jsonText, false);
} catch (IllegalArgumentException e) {
form.dispatchErrorOccurredEvent(this, "JsonTextDecode",
ErrorMessages.ERROR_WEB_JSON_TEXT_DECODE_FAILED, jsonText);
......@@ -874,46 +882,155 @@ public class Web extends AndroidNonvisibleComponent implements Component {
}
}
/**
* Decodes the given JSON encoded value to produce a corresponding App Inventor value.
* A JSON list [x, y, z] decodes to a list (x y z). A JSON Object with name A and value B,
* denoted as \{a: b\} decodes to a dictionary with the key a and value b.
*
* @param jsonText The JSON text to decode.
* @return The decoded value.
*/
@SimpleFunction
public Object JsonTextDecodeWithDictionaries(String jsonText) {
try {
return decodeJsonText(jsonText, true);
} catch (IllegalArgumentException e) {
form.dispatchErrorOccurredEvent(this, "JsonTextDecodeWithDictionaries",
ErrorMessages.ERROR_WEB_JSON_TEXT_DECODE_FAILED, jsonText);
return "";
}
}
/**
* Decodes the given JSON encoded value.
*
* @param jsonText the JSON text to decode
* @param useDicts <code>true</code> to repesent JSON objects using YailDictionaries or false to
* represent JSON objects using associative lists
* @return the decoded object
* @throws IllegalArgumentException if the JSON text can't be decoded
*/
// VisibleForTesting
static Object decodeJsonText(String jsonText) throws IllegalArgumentException {
@VisibleForTesting
static Object decodeJsonText(String jsonText, boolean useDicts) throws IllegalArgumentException {
try {
return JsonUtil.getObjectFromJson(jsonText);
return JsonUtil.getObjectFromJson(jsonText, useDicts);
} catch (JSONException e) {
throw new IllegalArgumentException("jsonText is not a legal JSON value");
}
}
/**
* Decodes the given JSON encoded value.
*
* @param jsonText the JSON text to decode
* @return the decoded object
* @throws IllegalArgumentException if the JSON text can't be decoded
* @deprecated As of nb182. Use {@link #decodeJsonText(String, boolean)} instead.
*/
@Deprecated
@VisibleForTesting
static Object decodeJsonText(String jsonText) throws IllegalArgumentException {
return decodeJsonText(jsonText, false);
}
/**
* Returns the value of a built-in type (i.e., boolean, number, text, list, dictionary)
* in its JavaScript Object Notation representation. If the value cannot be
* represented as JSON, the Screen's ErrorOccurred event will be run, if any,
* and the Web component will return the empty string.
*
* @param jsonObject the object to turn into JSON
* @return the stringified JSON value
*/
@SimpleFunction
public String JsonObjectEncode(Object jsonObject) {
try {
return JsonUtil.encodeJsonObject(jsonObject);
} catch (IllegalArgumentException e) {
form.dispatchErrorOccurredEvent(this, "JsonObjectEncode",
ErrorMessages.ERROR_WEB_JSON_TEXT_ENCODE_FAILED, jsonObject);
return "";
}
}
/**
* Decodes the given XML string to produce a dictionary structure. The dictionary includes the
* special keys `$tag`, `$localName`, `$namespace`, `$namespaceUri`, `$attributes`, and `$content`,
* as well as a key for each unique tag for every node, which points to a list of elements of
* the same structure as described here.
*
* The `$tag` key is the full tag name, e.g., foo:bar. The `$localName` is the local portion of
* the name (everything after the colon `:` character). If a namespace is given (everything before
* the colon `:` character), it is provided in `$namespace` and the corresponding URI is given
* in `$namespaceUri`. The attributes are stored in a dictionary in `$attributes` and the
* child nodes are given as a list under `$content`.
*
* **More Information on Special Keys**
*
* Consider the following XML document:
*
* ```xml
* <ex:Book xmlns:ex="http://example.com/">
* <ex:title xml:lang="en">On the Origin of Species</ex:title>
* <ex:author>Charles Darwin</ex:author>
* </ex:Book>
* ```
*
* When parsed, the `$tag` key will be `"ex:Book"`, the `$localName` key will be `"Book"`, the
* `$namespace` key will be `"ex"`, `$namespaceUri` will be `"http://example.com/"`, the
* `$attributes` key will be a dictionary `{}` (xmlns is removed for the namespace), and the
* `$content` will be a list of two items representing the decoded `<ex:title>` and `<ex:author>`
* elements. The first item, which corresponds to the `<ex:title>` element, will have an
* `$attributes` key containing the dictionary `{"xml:lang": "en"}`. For each `name=value`
* attribute on an element, a key-value pair mapping `name` to `value` will exist in the
* `$attributes` dictionary. In addition to these special keys, there will also be `"ex:title"`
* and `"ex:author"` to allow lookups faster than having to traverse the `$content` list.
*
* @param XmlText the JSON text to decode
* @return the decoded text
*/
@SimpleFunction(description = "Decodes the given XML into a set of nested dictionaries that " +
"capture the structure and data contained in the XML. See the help for more details.") public Object XMLTextDecodeAsDictionary(String XmlText) {
try {
XmlParser p = new XmlParser();
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
InputSource is = new InputSource(new StringReader(XmlText));
is.setEncoding("UTF-8");
parser.parse(is, p);
return p.getRoot();
} catch (Exception e) {
Log.e(LOG_TAG, e.getMessage());
form.dispatchErrorOccurredEvent(this, "XMLTextDecodeAsDictionary",
ErrorMessages.ERROR_WEB_JSON_TEXT_DECODE_FAILED, e.getMessage());
return new YailDictionary();
}
}
/**
* Decodes the given XML string to produce a list structure. `<tag>string</tag>` decodes to
* a list that contains a pair of tag and string. More generally, if obj1, obj2, ...
* are tag-delimited XML strings, then `<tag>obj1 obj2 ...</tag>` decodes to a list
* that contains a pair whose first element is tag and whose second element is the
* list of the decoded obj's, ordered alphabetically by tags. Examples:
* `<foo>123</foo>` decodes to a one-item list containing the pair (foo, 123)
* `<foo>1 2 3</foo>` decodes to a one-item list containing the pair (foo,"1 2 3")
* `<a><foo>1 2 3</foo><bar>456</bar></a>` decodes to a list containing the pair
* (a,X) where X is a 2-item list that contains the pair (bar,123) and the pair (foo,"1 2 3").
* If the sequence of obj's mixes tag-delimited and non-tag-delimited
* items, then the non-tag-delimited items are pulled out of the sequence and wrapped
* with a "content" tag. For example, decoding `<a><bar>456</bar>many<foo>1 2 3</foo>apples</a>`
* list of the decoded obj's, ordered alphabetically by tags.
*
* Examples:
* * `<foo><123/foo>` decodes to a one-item list containing the pair `(foo 123)`
* * `<foo>1 2 3</foo>` decodes to a one-item list containing the pair `(foo "1 2 3")`
* * `<a><foo>1 2 3</foo><bar>456</bar></a>` decodes to a list containing the pair `(a X)`
* where X is a 2-item list that contains the pair `(bar 123)` and the pair `(foo "1 2 3")`.
*
* If the sequence of obj's mixes tag-delimited and non-tag-delimited items, then the
* non-tag-delimited items are pulled out of the sequence and wrapped with a "content" tag.
* For example, decoding `<a><bar>456</bar>many<foo>1 2 3</foo>apples<a></code>`
* is similar to above, except that the list X is a 3-item list that contains the additional pair
* whose first item is the string "content", and whose second item is the list (many, apples).
* This method signals an error and returns the empty list if the result is not well-formed XML.
*
* See ["Working with XML and Web Services"](../other/xml.html) for more details.
*
* @param XmlText the XML text to decode
* @return the decoded text
*/
// This method works by by first converting the XML to JSON and then decoding the JSON.
@SimpleFunction(description = "Decodes the given XML string to produce a list structure. " +
@SimpleFunction(description = "Decodes the given XML string to produce a dictionary structure. " +
"See the App Inventor documentation on \"Other topics, notes, and details\" for information.")
// The above description string is punted because I can't figure out how to write the
// documentation string in a way that will look work both as a tooltip and in the autogenerated
......@@ -925,7 +1042,7 @@ public class Web extends AndroidNonvisibleComponent implements Component {
// We could be more precise and signal different errors for the conversion to JSON
// versus the decoding of that JSON, but showing the actual error message should
// be good enough.
Log.e("Exception in XMLTextDecode", e.getMessage());
Log.e(LOG_TAG, e.getMessage());
form.dispatchErrorOccurredEvent(this, "XMLTextDecode",
ErrorMessages.ERROR_WEB_JSON_TEXT_DECODE_FAILED, e.getMessage());
// This XMLTextDecode should always return a list, even in the case of an error
......@@ -937,7 +1054,7 @@ public class Web extends AndroidNonvisibleComponent implements Component {
* Decodes the given HTML text value.
*
* HTML Character Entities such as `&amp;`, `&lt;`, `&gt;`, `&apos;`, and `&quot;` are
* changed to &, <, >, ', and ".
* changed to `&`, `<`, `>`, `'`, and `"`.
* Entities such as `&#xhhhh;`, and `&#nnnn;` are changed to the appropriate characters.
*
* @param htmlText the HTML text to decode
......
......@@ -139,6 +139,7 @@ public final class ErrorMessages {
public static final int ERROR_WEB_UNABLE_TO_DELETE = 1114;
public static final int ERROR_WEB_XML_TEXT_DECODE_FAILED = 1115;
public static final int ERROR_WEB_REQUEST_TIMED_OUT = 1117; //Continuing from number after contact picker
public static final int ERROR_WEB_JSON_TEXT_ENCODE_FAILED = 1118;
// Contact picker (and PhoneNumberPicker) errors
public static final int ERROR_PHONE_UNSUPPORTED_CONTACT_PICKER = 1107;
public static final int ERROR_PHONE_UNSUPPORTED_SEARCH_IN_CONTACT_PICKING = 1108;
......@@ -508,6 +509,8 @@ public final class ErrorMessages {
"Unable to delete a resource with the specified URL: %s");
errorMessages.put(ERROR_WEB_REQUEST_TIMED_OUT,
"Took longer then timeout period to receive data from the URL: %s");
errorMessages.put(ERROR_WEB_JSON_TEXT_ENCODE_FAILED,
"Unable to encode as JSON the object %s");
// Contact picker (and PhoneNumberPicker) errors
errorMessages.put(ERROR_PHONE_UNSUPPORTED_CONTACT_PICKER,
"The software used in this app cannot extract contacts from this type of phone.");
......
......@@ -459,5 +459,20 @@ public class JsonUtil {
files[i].delete();
}
}
/**
* Encodes the given JSON object to a JSON string
*
* @param jsonObject the JSON object to encode
* @return the encoded string
* @throws IllegalArgumentException if the JSON object can't be encoded
*/
public static String encodeJsonObject(Object jsonObject) throws IllegalArgumentException {
try {
return getJsonRepresentation(jsonObject);
} catch (JSONException e) {
throw new IllegalArgumentException("jsonObject is not a legal JSON object");
}
}
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2019 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime.util;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
@SuppressWarnings("unchecked")
public class XmlParser extends DefaultHandler {
private static final String CONTENT_TAG = "$content";
private YailDictionary root = null;
private YailDictionary currentElement = null;
private Deque<YailDictionary> stack = new LinkedList<>();
@Override
public void startElement(String uri, String localName, String qname, Attributes attributes) {
YailDictionary el = new YailDictionary();
el.put("$tag", qname);
el.put("$namespaceUri", uri);
el.put("$localName", localName.isEmpty() ? qname : localName);
if (qname.contains(":")) {
String[] parts = qname.split(":");
el.put("$namespace", parts[0]);
} else {
el.put("$namespace", "");
}
YailDictionary attrs = new YailDictionary();
for (int i = 0; i < attributes.getLength(); i++) {
attrs.put(attributes.getQName(i), attributes.getValue(i));
}
el.put("$attributes", attrs);
el.put(CONTENT_TAG, new ArrayList<>());
if (currentElement != null) {
((List<Object>) currentElement.get(CONTENT_TAG)).add(el);
if (!currentElement.containsKey(qname)) {
currentElement.put(qname, new ArrayList<>());
}
((List<Object>) currentElement.get(qname)).add(el);
stack.push(currentElement);
} else {
root = el;
}
currentElement = el;
}
@Override
public void characters(char[] ch, int start, int length) {
List<Object> items = (List<Object>) currentElement.get(CONTENT_TAG);
if (items instanceof ArrayList) {
String content = new String(ch, start, length);
content = content.trim();
if (!content.isEmpty()) {
items.add(content);
}
}
}
@Override
public void endElement(String uri, String localName, String qname) {
for (Entry<Object, Object> e : currentElement.entrySet()) {
if (e.getValue() instanceof ArrayList) {
e.setValue(YailList.makeList((List<?>) e.getValue()));
}
}
if (!stack.isEmpty()) {
currentElement = stack.pop();
}
}
public YailDictionary getRoot() {
return root;
}
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2019 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.YailDictionary;
import com.google.appinventor.components.runtime.util.YailList;
import org.junit.Before;
......@@ -16,7 +17,11 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
......@@ -34,57 +39,53 @@ public class WebTest {
private Web web;
@Before
public void setUp() throws Exception {
public void setUp() {
web = new Web();
}
@Test
public void testDecodeJsonText() throws Exception {
public void testDecodeJsonText() {
// String values.
assertEquals("\t tab \t tab \t",
web.decodeJsonText("\"\\t tab \\t tab \\t\""));
Web.decodeJsonText("\"\\t tab \\t tab \\t\"", true));
assertEquals("\n newline \n newline \n",
web.decodeJsonText("\"\\n newline \\n newline \\n\""));
Web.decodeJsonText("\"\\n newline \\n newline \\n\"", true));
assertEquals("/ slash / slash /",
web.decodeJsonText("\"\\/ slash \\/ slash \\/\""));
Web.decodeJsonText("\"\\/ slash \\/ slash \\/\"", true));
assertEquals("\\ backslash \\ backslash \\",
web.decodeJsonText("\"\\\\ backslash \\\\ backslash \\\\\""));
Web.decodeJsonText("\"\\\\ backslash \\\\ backslash \\\\\"", true));
assertEquals("\" quote \" quote \"",
web.decodeJsonText("\"\\\" quote \\\" quote \\\"\""));
Web.decodeJsonText("\"\\\" quote \\\" quote \\\"\"", true));
assertEquals("~ encoded tilda ~ encoded tilda ~",
web.decodeJsonText("\"\\u007E encoded tilda \\u007E encoded tilda \\u007E\""));
Web.decodeJsonText("\"\\u007E encoded tilda \\u007E encoded tilda \\u007E\"", true));
// Boolean values.
assertEquals(Boolean.TRUE, web.decodeJsonText("True"));
assertEquals(Boolean.FALSE, web.decodeJsonText("False"));
assertEquals(Boolean.TRUE, Web.decodeJsonText("True", true));
assertEquals(Boolean.FALSE, Web.decodeJsonText("False", true));
// Numeric values.
assertEquals(new Integer(1), web.decodeJsonText("1"));
assertEquals(new Double(57.43), web.decodeJsonText("57.43"));
assertEquals(1, Web.decodeJsonText("1", true));
assertEquals(57.43, Web.decodeJsonText("57.43", true));
// A JSON encoded object.
Object decodedObject = web.decodeJsonText("{\"YaVersion\":\"41\",\"Source\":\"Form\"}");
assertTrue(decodedObject instanceof ArrayList);
ArrayList outerList = (ArrayList) decodedObject;
Object decodedObject = Web.decodeJsonText("{\"YaVersion\":\"41\",\"Source\":\"Form\"}", true);
assertTrue(decodedObject instanceof Map);
YailDictionary outerList = (YailDictionary) decodedObject;
assertEquals(2, outerList.size());
// The items are sorted by the field name, so Source comes before YaVersion
Object item0 = outerList.get(0);
assertTrue(item0 instanceof ArrayList);
ArrayList firstNameValuePair = (ArrayList) item0;
assertEquals(2, firstNameValuePair.size());
assertEquals("Source", firstNameValuePair.get(0));
assertEquals("Form", firstNameValuePair.get(1));
Object item1 = outerList.get(1);
assertTrue(item1 instanceof ArrayList);
ArrayList secondNameValuePair = (ArrayList) item1;
assertEquals(2, secondNameValuePair.size());
assertEquals("YaVersion", secondNameValuePair.get(0));
assertEquals("41", secondNameValuePair.get(1));
Iterator<Entry<Object, Object>> it = outerList.entrySet().iterator();
Entry<Object, Object> item0 = it.next();
assertEquals("Source", item0.getKey());
assertEquals("Form", item0.getValue());
Entry<Object, Object> item1 = it.next();
assertEquals("YaVersion", item1.getKey());
assertEquals("41", item1.getValue());
// A JSON encoded array.
Object decodedArray = web.decodeJsonText("[\"Billy\",\"Sam\",\"Bobby\",\"Fred\"]");
Object decodedArray = Web.decodeJsonText("[\"Billy\",\"Sam\",\"Bobby\",\"Fred\"]", true);
assertTrue(decodedArray instanceof ArrayList);
ArrayList list = (ArrayList) decodedArray;
@SuppressWarnings("unchecked")
ArrayList<Object> list = (ArrayList<Object>) decodedArray;
assertEquals(4, list.size());
assertEquals("Billy", list.get(0));
assertEquals("Sam", list.get(1));
......@@ -92,7 +93,7 @@ public class WebTest {
assertEquals("Fred", list.get(3));
try {
web.decodeJsonText("{\"not\":\"valid\":\"json\"}");
Web.decodeJsonText("{\"not\":\"valid\":\"json\"}", true);
fail();
} catch (IllegalArgumentException e) {
// Expected.
......@@ -104,11 +105,11 @@ public class WebTest {
Object decodedObject = web.XMLTextDecode("<foo>123</foo>");
// should be the list of one element, which is a pair of "foo" and 123
assertTrue(decodedObject instanceof ArrayList);
ArrayList outerList = (ArrayList) decodedObject;
ArrayList<?> outerList = (ArrayList<?>) decodedObject;
assertEquals(1, outerList.size());
Object pairObject = outerList.get(0);
assertTrue(pairObject instanceof ArrayList);
ArrayList pair = (ArrayList) pairObject;
ArrayList<?> pair = (ArrayList<?>) pairObject;
assertEquals(2, pair.size());
assertEquals("foo", pair.get(0));
// check why this isn't actually the string 123
......@@ -124,23 +125,23 @@ public class WebTest {
// The order of these is bar before foo because it's alphabetical by according to
// the tags.
assertTrue(decodedObject instanceof ArrayList);
ArrayList outerList = (ArrayList) decodedObject;
ArrayList<?> outerList = (ArrayList<?>) decodedObject;
assertEquals(1, outerList.size());
Object pairObject = outerList.get(0);
assertTrue(pairObject instanceof ArrayList);
ArrayList pair = (ArrayList) pairObject;
ArrayList<?> pair = (ArrayList<?>) pairObject;
assertEquals(2, pair.size());
assertEquals("a", pair.get(0));
Object XObject = pair.get(1);
assertTrue(XObject instanceof ArrayList);
ArrayList X = (ArrayList) XObject;
ArrayList<?> X = (ArrayList<?>) XObject;
assertEquals(2, X.size());
Object firstPairObject = X.get(0);
Object secondPairObject = X.get(1);
assertTrue(firstPairObject instanceof ArrayList);
assertTrue(secondPairObject instanceof ArrayList);
ArrayList firstPair = (ArrayList) firstPairObject;
ArrayList secondPair = (ArrayList) secondPairObject;
ArrayList<?> firstPair = (ArrayList<?>) firstPairObject;
ArrayList<?> secondPair = (ArrayList<?>) secondPairObject;
assertEquals("bar", firstPair.get(0));
assertEquals(456, firstPair.get(1));
assertEquals("foo", secondPair.get(0));
......@@ -148,13 +149,13 @@ public class WebTest {
}
@Test
public void testDecodeXMLCDATA() throws Exception {
public void testDecodeXMLCDATA() {
Object decodedObject = web.XMLTextDecode("<xml><![CDATA[foo < bar || bar > baz]]></xml>");
assertTrue(decodedObject instanceof List);
List outerList = (List) decodedObject;
List<?> outerList = (List<?>) decodedObject;
assertEquals(1, outerList.size());
assertTrue(outerList.get(0) instanceof List);
List tagValuePair = (List) ((List) decodedObject).get(0);
List<?> tagValuePair = (List<?>) ((List<?>) decodedObject).get(0);
assertEquals(2, tagValuePair.size());
// tag should be xml
assertEquals("xml", tagValuePair.get(0));
......@@ -163,8 +164,41 @@ public class WebTest {
}
@Test
public void testbuildRequestData() throws Exception {
List<Object> list = new ArrayList<Object>();
public void testDecodeXMLTextAsDictionary() {
Object decodedObject = web.XMLTextDecodeAsDictionary("<foo>123</foo>");
// Should be a dictionary mapping "foo" to 123
assertTrue(decodedObject instanceof YailDictionary);
YailDictionary outerList = (YailDictionary) decodedObject;
// Dictionary has fields $tag, $namespaceUri, $localName, $namespace, $attributes, $content
assertEquals(6, outerList.size());
assertEquals("foo", outerList.get("$tag"));
assertTrue(outerList.get("$content") instanceof List);
assertEquals(YailList.makeList(new Object[] { "123" }), outerList.get("$content"));
}
@Test
public void testDecodeXMLTextAsDictionary2() {
Object decodedObject = web.XMLTextDecodeAsDictionary("<a><foo>1 2 3</foo><bar>456</bar></a>");
// should be the list of one element, which is a pair of "a" and a list X.
// X is a list two pairs. The first pair is "bar" and 456 and the second pair is
// "foo" and the string "1 2 3".
// The order of these is bar before foo because it's alphabetical by according to
// the tags.
assertTrue(decodedObject instanceof YailDictionary);
YailDictionary result = (YailDictionary) decodedObject;
assertEquals(8, result.size());
assertTrue(result.containsKey("foo"));
assertTrue(result.get("foo") instanceof YailList);
assertTrue(result.containsKey("bar"));
assertTrue(result.get("bar") instanceof YailList);
assertEquals(2, ((YailList) result.get("$content")).size());
assertEquals("1 2 3", result.getObjectAtKeyPath(Arrays.asList("foo", 1, "$content", 1)));
assertEquals("456", result.getObjectAtKeyPath(Arrays.asList("bar", 1, "$content", 1)));
}
@Test
public void testBuildRequestData() throws Exception {
List<Object> list = new ArrayList<>();
list.add(YailList.makeList(new String[] { "First Name", "Barack" }));
list.add(YailList.makeList(new String[] { "Last Name", "Obama" }));
list.add(YailList.makeList(new String[] { "Title", "President of the United States" }));
......
......@@ -539,14 +539,27 @@ set the following properties:
<dd>Decodes the given HTML text value.
<p>HTML Character Entities such as <code class="highlighter-rouge">&amp;amp;</code>, <code class="highlighter-rouge">&amp;lt;</code>, <code class="highlighter-rouge">&amp;gt;</code>, <code class="highlighter-rouge">&amp;apos;</code>, and <code class="highlighter-rouge">&amp;quot;</code> are
changed to &amp;, &lt;, &gt;, ‘, and “.
changed to <code class="highlighter-rouge">&amp;</code>, <code class="highlighter-rouge">&lt;</code>, <code class="highlighter-rouge">&gt;</code>, <code class="highlighter-rouge">'</code>, and <code class="highlighter-rouge">"</code>.
Entities such as <code class="highlighter-rouge">&amp;#xhhhh;</code>, and <code class="highlighter-rouge">&amp;#nnnn;</code> are changed to the appropriate characters.</p>
</dd>
<dt id="Web.JsonObjectEncode" class="method returns text"><i></i> JsonObjectEncode(<em class="any">jsonObject</em>)</dt>
<dd>Returns the value of a built-in type (i.e., boolean, number, text, list, dictionary)
in its JavaScript Object Notation representation. If the value cannot be
represented as JSON, the Screen’s ErrorOccurred event will be run, if any,
and the Web component will return the empty string.</dd>
<dt id="Web.JsonTextDecode" class="method returns any"><i></i> JsonTextDecode(<em class="text">jsonText</em>)</dt>
<dd>Decodes the given JSON encoded value to produce a corresponding AppInventor value.
A JSON list <code class="highlighter-rouge">[x, y, z]</code> decodes to a list <code class="highlighter-rouge">(x y z)</code>, A JSON object with key A and value B,
(denoted as <code class="highlighter-rouge">{A:B}</code>) decodes to a list <code class="highlighter-rouge">((A B))</code>, that is, a list containing the two-element
list <code class="highlighter-rouge">(A B)</code>.</dd>
list <code class="highlighter-rouge">(A B)</code>.
<p>Use the method <a href="#Web.JsonTextDecodeWithDictionaries">JsonTextDecodeWithDictionaries</a> if you
would prefer to get back dictionary objects rather than lists-of-lists in the result.</p>
</dd>
<dt id="Web.JsonTextDecodeWithDictionaries" class="method returns any"><i></i> JsonTextDecodeWithDictionaries(<em class="text">jsonText</em>)</dt>
<dd>Decodes the given JSON encoded value to produce a corresponding App Inventor value.
A JSON list [x, y, z] decodes to a list (x y z). A JSON Object with name A and value B,
denoted as {a: b} decodes to a dictionary with the key a and value b.</dd>
<dt id="Web.PostFile" class="method"><i></i> PostFile(<em class="text">path</em>)</dt>
<dd>Performs an HTTP POST request using the Url property and data from the specified file.
......@@ -618,19 +631,54 @@ set the following properties:
a list that contains a pair of tag and string. More generally, if obj1, obj2, …
are tag-delimited XML strings, then <code class="highlighter-rouge">&lt;tag&gt;obj1 obj2 ...&lt;/tag&gt;</code> decodes to a list
that contains a pair whose first element is tag and whose second element is the
list of the decoded obj’s, ordered alphabetically by tags. Examples:
<code class="highlighter-rouge">&lt;foo&gt;123&lt;/foo&gt;</code> decodes to a one-item list containing the pair (foo, 123)
<code class="highlighter-rouge">&lt;foo&gt;1 2 3&lt;/foo&gt;</code> decodes to a one-item list containing the pair (foo,”1 2 3”)
<code class="highlighter-rouge">&lt;a&gt;&lt;foo&gt;1 2 3&lt;/foo&gt;&lt;bar&gt;456&lt;/bar&gt;&lt;/a&gt;</code> decodes to a list containing the pair
(a,X) where X is a 2-item list that contains the pair (bar,123) and the pair (foo,”1 2 3”).
If the sequence of obj’s mixes tag-delimited and non-tag-delimited
items, then the non-tag-delimited items are pulled out of the sequence and wrapped
with a “content” tag. For example, decoding <code class="highlighter-rouge">&lt;a&gt;&lt;bar&gt;456&lt;/bar&gt;many&lt;foo&gt;1 2 3&lt;/foo&gt;apples&lt;/a&gt;</code>
list of the decoded obj’s, ordered alphabetically by tags.
<p>Examples:</p>
<ul>
<li><code class="highlighter-rouge">&lt;foo&gt;&lt;123/foo&gt;</code> decodes to a one-item list containing the pair <code class="highlighter-rouge">(foo 123)</code></li>
<li><code class="highlighter-rouge">&lt;foo&gt;1 2 3&lt;/foo&gt;</code> decodes to a one-item list containing the pair <code class="highlighter-rouge">(foo "1 2 3")</code></li>
<li><code class="highlighter-rouge">&lt;a&gt;&lt;foo&gt;1 2 3&lt;/foo&gt;&lt;bar&gt;456&lt;/bar&gt;&lt;/a&gt;</code> decodes to a list containing the pair <code class="highlighter-rouge">(a X)</code>
where X is a 2-item list that contains the pair <code class="highlighter-rouge">(bar 123)</code> and the pair <code class="highlighter-rouge">(foo "1 2 3")</code>.</li>
</ul>
<p>If the sequence of obj’s mixes tag-delimited and non-tag-delimited items, then the
non-tag-delimited items are pulled out of the sequence and wrapped with a “content” tag.
For example, decoding <code class="highlighter-rouge">&lt;a&gt;&lt;bar&gt;456&lt;/bar&gt;many&lt;foo&gt;1 2 3&lt;/foo&gt;apples&lt;a&gt;&lt;/code&gt;</code>
is similar to above, except that the list X is a 3-item list that contains the additional pair
whose first item is the string “content”, and whose second item is the list (many, apples).
This method signals an error and returns the empty list if the result is not well-formed XML.
<p>See <a href="../other/xml.html">“Working with XML and Web Services”</a> for more details.</p>
This method signals an error and returns the empty list if the result is not well-formed XML.</p>
</dd>
<dt id="Web.XMLTextDecodeAsDictionary" class="method returns any"><i></i> XMLTextDecodeAsDictionary(<em class="text">XmlText</em>)</dt>
<dd>Decodes the given XML string to produce a dictionary structure. The dictionary includes the
special keys <code class="highlighter-rouge">$tag</code>, <code class="highlighter-rouge">$localName</code>, <code class="highlighter-rouge">$namespace</code>, <code class="highlighter-rouge">$namespaceUri</code>, <code class="highlighter-rouge">$attributes</code>, and <code class="highlighter-rouge">$content</code>,
as well as a key for each unique tag for every node, which points to a list of elements of
the same structure as described here.
<p>The <code class="highlighter-rouge">$tag</code> key is the full tag name, e.g., foo:bar. The <code class="highlighter-rouge">$localName</code> is the local portion of
the name (everything after the colon <code class="highlighter-rouge">:</code> character). If a namespace is given (everything before
the colon <code class="highlighter-rouge">:</code> character), it is provided in <code class="highlighter-rouge">$namespace</code> and the corresponding URI is given
in <code class="highlighter-rouge">$namespaceUri</code>. The attributes are stored in a dictionary in <code class="highlighter-rouge">$attributes</code> and the
child nodes are given as a list under <code class="highlighter-rouge">$content</code>.</p>
<p><strong>More Information on Special Keys</strong></p>
<p>Consider the following XML document:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;ex:Book</span> <span class="na">xmlns:ex=</span><span class="s">"http://example.com/"</span><span class="nt">&gt;</span>
<span class="nt">&lt;ex:title</span> <span class="na">xml:lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>On the Origin of Species<span class="nt">&lt;/ex:title&gt;</span>
<span class="nt">&lt;ex:author&gt;</span>Charles Darwin<span class="nt">&lt;/ex:author&gt;</span>
<span class="nt">&lt;/ex:Book&gt;</span>
</code></pre></div> </div>
<p>When parsed, the <code class="highlighter-rouge">$tag</code> key will be <code class="highlighter-rouge">"ex:Book"</code>, the <code class="highlighter-rouge">$localName</code> key will be <code class="highlighter-rouge">"Book"</code>, the
<code class="highlighter-rouge">$namespace</code> key will be <code class="highlighter-rouge">"ex"</code>, <code class="highlighter-rouge">$namespaceUri</code> will be <code class="highlighter-rouge">"http://example.com/"</code>, the
<code class="highlighter-rouge">$attributes</code> key will be a dictionary <code class="highlighter-rouge">{}</code> (xmlns is removed for the namespace), and the
<code class="highlighter-rouge">$content</code> will be a list of two items representing the decoded <code class="highlighter-rouge">&lt;ex:title&gt;</code> and <code class="highlighter-rouge">&lt;ex:author&gt;</code>
elements. The first item, which corresponds to the <code class="highlighter-rouge">&lt;ex:title&gt;</code> element, will have an
<code class="highlighter-rouge">$attributes</code> key containing the dictionary <code class="highlighter-rouge">{"xml:lang": "en"}</code>. For each <code class="highlighter-rouge">name=value</code>
attribute on an element, a key-value pair mapping <code class="highlighter-rouge">name</code> to <code class="highlighter-rouge">value</code> will exist in the
<code class="highlighter-rouge">$attributes</code> dictionary. In addition to these special keys, there will also be <code class="highlighter-rouge">"ex:title"</code>
and <code class="highlighter-rouge">"ex:author"</code> to allow lookups faster than having to traverse the <code class="highlighter-rouge">$content</code> list.</p>
</dd>
</dl>
......
......@@ -470,15 +470,29 @@ Non-visible component that provides functions for HTTP GET, POST, PUT, and DELET
: Decodes the given HTML text value.
HTML Character Entities such as `&amp;`, `&lt;`, `&gt;`, `&apos;`, and `&quot;` are
changed to &, <, >, ', and ".
changed to `&`, `<`, `>`, `'`, and `"`.
Entities such as `&#xhhhh;`, and `&#nnnn;` are changed to the appropriate characters.
{:id="Web.JsonObjectEncode" class="method returns text"} <i/> JsonObjectEncode(*jsonObject*{:.any})
: Returns the value of a built-in type (i.e., boolean, number, text, list, dictionary)
in its JavaScript Object Notation representation. If the value cannot be
represented as JSON, the Screen's ErrorOccurred event will be run, if any,
and the Web component will return the empty string.
{:id="Web.JsonTextDecode" class="method returns any"} <i/> JsonTextDecode(*jsonText*{:.text})
: Decodes the given JSON encoded value to produce a corresponding AppInventor value.
A JSON list `[x, y, z]` decodes to a list `(x y z)`, A JSON object with key A and value B,
(denoted as `{A:B}`) decodes to a list `((A B))`, that is, a list containing the two-element
list `(A B)`.
Use the method [JsonTextDecodeWithDictionaries](#Web.JsonTextDecodeWithDictionaries) if you
would prefer to get back dictionary objects rather than lists-of-lists in the result.
{:id="Web.JsonTextDecodeWithDictionaries" class="method returns any"} <i/> JsonTextDecodeWithDictionaries(*jsonText*{:.text})
: Decodes the given JSON encoded value to produce a corresponding App Inventor value.
A JSON list [x, y, z] decodes to a list (x y z). A JSON Object with name A and value B,
denoted as \{a: b\} decodes to a dictionary with the key a and value b.
{:id="Web.PostFile" class="method"} <i/> PostFile(*path*{:.text})
: Performs an HTTP POST request using the Url property and data from the specified file.
......@@ -552,16 +566,50 @@ Non-visible component that provides functions for HTTP GET, POST, PUT, and DELET
a list that contains a pair of tag and string. More generally, if obj1, obj2, ...
are tag-delimited XML strings, then `<tag>obj1 obj2 ...</tag>` decodes to a list
that contains a pair whose first element is tag and whose second element is the
list of the decoded obj's, ordered alphabetically by tags. Examples:
`<foo>123</foo>` decodes to a one-item list containing the pair (foo, 123)
`<foo>1 2 3</foo>` decodes to a one-item list containing the pair (foo,"1 2 3")
`<a><foo>1 2 3</foo><bar>456</bar></a>` decodes to a list containing the pair
(a,X) where X is a 2-item list that contains the pair (bar,123) and the pair (foo,"1 2 3").
If the sequence of obj's mixes tag-delimited and non-tag-delimited
items, then the non-tag-delimited items are pulled out of the sequence and wrapped
with a "content" tag. For example, decoding `<a><bar>456</bar>many<foo>1 2 3</foo>apples</a>`
list of the decoded obj's, ordered alphabetically by tags.
Examples:
* `<foo><123/foo>` decodes to a one-item list containing the pair `(foo 123)`
* `<foo>1 2 3</foo>` decodes to a one-item list containing the pair `(foo "1 2 3")`
* `<a><foo>1 2 3</foo><bar>456</bar></a>` decodes to a list containing the pair `(a X)`
where X is a 2-item list that contains the pair `(bar 123)` and the pair `(foo "1 2 3")`.
If the sequence of obj's mixes tag-delimited and non-tag-delimited items, then the
non-tag-delimited items are pulled out of the sequence and wrapped with a "content" tag.
For example, decoding `<a><bar>456</bar>many<foo>1 2 3</foo>apples<a></code>`
is similar to above, except that the list X is a 3-item list that contains the additional pair
whose first item is the string "content", and whose second item is the list (many, apples).
This method signals an error and returns the empty list if the result is not well-formed XML.
See ["Working with XML and Web Services"](../other/xml.html) for more details.
{:id="Web.XMLTextDecodeAsDictionary" class="method returns any"} <i/> XMLTextDecodeAsDictionary(*XmlText*{:.text})
: Decodes the given XML string to produce a dictionary structure. The dictionary includes the
special keys `$tag`, `$localName`, `$namespace`, `$namespaceUri`, `$attributes`, and `$content`,
as well as a key for each unique tag for every node, which points to a list of elements of
the same structure as described here.
The `$tag` key is the full tag name, e.g., foo:bar. The `$localName` is the local portion of
the name (everything after the colon `:` character). If a namespace is given (everything before
the colon `:` character), it is provided in `$namespace` and the corresponding URI is given
in `$namespaceUri`. The attributes are stored in a dictionary in `$attributes` and the
child nodes are given as a list under `$content`.
**More Information on Special Keys**
Consider the following XML document:
```xml
<ex:Book xmlns:ex="http://example.com/">
<ex:title xml:lang="en">On the Origin of Species</ex:title>
<ex:author>Charles Darwin</ex:author>
</ex:Book>
```
When parsed, the `$tag` key will be `"ex:Book"`, the `$localName` key will be `"Book"`, the
`$namespace` key will be `"ex"`, `$namespaceUri` will be `"http://example.com/"`, the
`$attributes` key will be a dictionary `{}` (xmlns is removed for the namespace), and the
`$content` will be a list of two items representing the decoded `<ex:title>` and `<ex:author>`
elements. The first item, which corresponds to the `<ex:title>` element, will have an
`$attributes` key containing the dictionary `{"xml:lang": "en"}`. For each `name=value`
attribute on an element, a key-value pair mapping `name` to `value` will exist in the
`$attributes` dictionary. In addition to these special keys, there will also be `"ex:title"`
and `"ex:author"` to allow lookups faster than having to traverse the `$content` list.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment