From d75f861024a7057ecb296e4849a478aaaea2de72 Mon Sep 17 00:00:00 2001 From: keyongyu <247321453@qq.com> Date: Thu, 25 Feb 2016 10:21:57 +0800 Subject: [PATCH 1/3] xml escape characters --- .gitignore | 1 + src/main/java/com/stanfy/gsonxml/GsonXml.java | 27 ++++++++++++++----- .../java/com/stanfy/gsonxml/XmlReader.java | 2 +- .../java/com/stanfy/gsonxml/test/RssTest.java | 27 ++++++++++--------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index a0c8aa9..e35f074 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ classes # Other /nbproject/private/ internal +/*.properties diff --git a/src/main/java/com/stanfy/gsonxml/GsonXml.java b/src/main/java/com/stanfy/gsonxml/GsonXml.java index 0b1fced..ee2bd1c 100644 --- a/src/main/java/com/stanfy/gsonxml/GsonXml.java +++ b/src/main/java/com/stanfy/gsonxml/GsonXml.java @@ -1,10 +1,5 @@ package com.stanfy.gsonxml; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.lang.reflect.Type; - import com.google.gson.Gson; import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; @@ -14,12 +9,18 @@ import com.google.gson.stream.MalformedJsonException; import com.stanfy.gsonxml.XmlReader.Options; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.lang.reflect.Type; + /** * Wrapper for {@link Gson}. * @author Roman Mazur (Stanfy - http://stanfy.com) */ public class GsonXml { - + public static final String[] CHAR_ORIGINAL = new String[]{"$", "<", ">", "\"", "'"}; + public static final String[] CHAR_TO = new String[]{"&", "<", ">", """, "'"}; /** Core object. */ private final Gson core; @@ -95,7 +96,19 @@ private static void assertFullConsumption(final Object obj, final JsonReader rea public T fromXml(final XmlReader reader, final Type typeOfT) throws JsonIOException, JsonSyntaxException { return core.fromJson(reader, typeOfT); } - + /** + * @param value + * @return + */ + static String decode(String value) { + if (value == null) return null; + for (int i = 0; i < CHAR_TO.length && i < CHAR_ORIGINAL.length; i++) { + String to = CHAR_TO[i]; + String original = CHAR_ORIGINAL[i]; + value = value.replace(to, original); + } + return value; + } @Override public String toString() { return core.toString(); } diff --git a/src/main/java/com/stanfy/gsonxml/XmlReader.java b/src/main/java/com/stanfy/gsonxml/XmlReader.java index bd8db16..4f33451 100644 --- a/src/main/java/com/stanfy/gsonxml/XmlReader.java +++ b/src/main/java/com/stanfy/gsonxml/XmlReader.java @@ -309,7 +309,7 @@ public JsonToken peek() throws IOException { @Override public String nextString() throws IOException { expect(JsonToken.STRING); - return nextValue().value; + return GsonXml.decode(nextValue().value); } @Override public boolean nextBoolean() throws IOException { diff --git a/src/test/java/com/stanfy/gsonxml/test/RssTest.java b/src/test/java/com/stanfy/gsonxml/test/RssTest.java index bcf8a6a..6257257 100644 --- a/src/test/java/com/stanfy/gsonxml/test/RssTest.java +++ b/src/test/java/com/stanfy/gsonxml/test/RssTest.java @@ -1,18 +1,19 @@ package com.stanfy.gsonxml.test; -import static org.junit.Assert.assertEquals; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import com.stanfy.gsonxml.GsonXml; +import com.stanfy.gsonxml.GsonXmlBuilder; + +import org.junit.Test; import java.io.InputStreamReader; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Locale; -import org.junit.Test; - -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.SerializedName; -import com.stanfy.gsonxml.GsonXml; -import com.stanfy.gsonxml.GsonXmlBuilder; +import static org.junit.Assert.assertEquals; public class RssTest extends AbstractXmlTest { @@ -21,11 +22,11 @@ public class RssTest extends AbstractXmlTest { "" + "\n" + " \n" - + " channel title" + + " channel title<" + " \n" + " 1\n" + " \n" - + " <![CDATA[Some text]]>\n" + + " <![CDATA[Some text<"]]>\n" + " \n" + " \n" + " " @@ -57,7 +58,9 @@ public static class Item { public void rssTest() throws Exception { final Rss feed = createGsonXml().fromXml(XML, Rss.class); assertEquals(1, feed.channel.items.get(0).id); - assertEquals(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").parse("Tue, 10 Jul 2012 10:43:36 +0300"), feed.channel.items.get(0).pubDate); + assertEquals("Some text<\"", feed.channel.items.get(0).title); + assertEquals("channel title<", feed.channel.title); + assertEquals(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US).parse("Tue, 10 Jul 2012 10:43:36 +0300"), feed.channel.items.get(0).pubDate); } @Test @@ -69,8 +72,8 @@ public void realTest() throws Exception { private GsonXml createGsonXml() { return new GsonXmlBuilder() .wrap( - new GsonBuilder().setDateFormat("EEE, dd MMM yyyy HH:mm:ss Z") - ) + new GsonBuilder().setDateFormat("EEE, dd MMM yyyy HH:mm:ss Z") + ) .setXmlParserCreator(SimpleXmlReaderTest.PARSER_CREATOR) .setSameNameLists(true) .create(); From d531b70e70f84e8631a435e4d2578cf5eacd57a8 Mon Sep 17 00:00:00 2001 From: keyongyu <247321453@qq.com> Date: Thu, 25 Feb 2016 13:04:49 +0800 Subject: [PATCH 2/3] subclass test support map --- .gitignore | 1 - build.gradle | 2 +- .../bind/XmlMapTypeAdapterFactory.java | 292 ++++ .../com/stanfy/gsonxml/GsonXmlBuilder.java | 362 ++-- .../java/com/stanfy/gsonxml/XmlReader.java | 1553 +++++++++-------- .../com/stanfy/gsonxml/test/DemoTest.java | 175 ++ .../com/stanfy/gsonxml/test/SubClassTest.java | 40 + 7 files changed, 1520 insertions(+), 905 deletions(-) create mode 100644 src/main/java/com/google/gson/internal/bind/XmlMapTypeAdapterFactory.java create mode 100644 src/test/java/com/stanfy/gsonxml/test/DemoTest.java create mode 100644 src/test/java/com/stanfy/gsonxml/test/SubClassTest.java diff --git a/.gitignore b/.gitignore index e35f074..bf5296f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,4 @@ classes # Other /nbproject/private/ -internal /*.properties diff --git a/build.gradle b/build.gradle index 8c65494..1b6722c 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ apply plugin : 'mattock' apply plugin : 'nexus' group = 'com.stanfy' -version = '0.1.8-SNAPSHOT' +version = '0.1.9-SNAPSHOT' repositories { mavenCentral() diff --git a/src/main/java/com/google/gson/internal/bind/XmlMapTypeAdapterFactory.java b/src/main/java/com/google/gson/internal/bind/XmlMapTypeAdapterFactory.java new file mode 100644 index 0000000..20e0f66 --- /dev/null +++ b/src/main/java/com/google/gson/internal/bind/XmlMapTypeAdapterFactory.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed 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 com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Adapts maps to either JSON objects or JSON arrays. + * + * <h3>Maps as JSON objects</h3> + * For primitive keys or when complex map key serialization is not enabled, this + * converts Java {@link Map Maps} to JSON Objects. This requires that map keys + * can be serialized as strings; this is insufficient for some key types. For + * example, consider a map whose keys are points on a grid. The default JSON + * form encodes reasonably: <pre> {@code + * Map<Point, String> original = new LinkedHashMap<Point, String>(); + * original.put(new Point(5, 6), "a"); + * original.put(new Point(8, 8), "b"); + * System.out.println(gson.toJson(original, type)); + * }</pre> + * The above code prints this JSON object:<pre> {@code + * { + * "(5,6)": "a", + * "(8,8)": "b" + * } + * }</pre> + * But GSON is unable to deserialize this value because the JSON string name is + * just the {@link Object#toString() toString()} of the map key. Attempting to + * convert the above JSON to an object fails with a parse exception: + * <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)" + * at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler + * at com.google.gson.ObjectNavigator.navigateClassFields + * ...</pre> + * + * <h3>Maps as JSON arrays</h3> + * An alternative approach taken by this type adapter when it is required and + * complex map key serialization is enabled is to encode maps as arrays of map + * entries. Each map entry is a two element array containing a key and a value. + * This approach is more flexible because any type can be used as the map's key; + * not just strings. But it's also less portable because the receiver of such + * JSON must be aware of the map entry convention. + * + * <p>Register this adapter when you are creating your GSON instance. + * <pre> {@code + * Gson gson = new GsonBuilder() + * .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter()) + * .create(); + * }</pre> + * This will change the structure of the JSON emitted by the code above. Now we + * get an array. In this case the arrays elements are map entries: + * <pre> {@code + * [ + * [ + * { + * "x": 5, + * "y": 6 + * }, + * "a", + * ], + * [ + * { + * "x": 8, + * "y": 8 + * }, + * "b" + * ] + * ] + * }</pre> + * This format will serialize and deserialize just fine as long as this adapter + * is registered. + */ +public final class XmlMapTypeAdapterFactory implements TypeAdapterFactory { + private final ConstructorConstructor constructorConstructor; + final boolean complexMapKeySerialization; + public static final String KEY_NAME = "key"; + public static final String VALUE_NAME = "value"; + + public XmlMapTypeAdapterFactory(ConstructorConstructor constructorConstructor, + boolean complexMapKeySerialization) { + this.constructorConstructor = constructorConstructor; + this.complexMapKeySerialization = complexMapKeySerialization; + } + + @Override + public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { + Type type = typeToken.getType(); + + Class<? super T> rawType = typeToken.getRawType(); + if (!Map.class.isAssignableFrom(rawType)) { + return null; + } + + Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type); + Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); + TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]); + TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1])); + ObjectConstructor<T> constructor = constructorConstructor.get(typeToken); + + @SuppressWarnings({"unchecked", "rawtypes"}) + // we don't define a type parameter for the key or value types + TypeAdapter<T> result = new Adapter(gson, keyAndValueTypes[0], keyAdapter, + keyAndValueTypes[1], valueAdapter, constructor); + return result; + } + + /** + * Returns a type adapter that writes the value as a string. + */ + private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) { + return (keyType == boolean.class || keyType == Boolean.class) + ? TypeAdapters.BOOLEAN_AS_STRING + : context.getAdapter(TypeToken.get(keyType)); + } + + private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> { + private final TypeAdapter<K> keyTypeAdapter; + private final TypeAdapter<V> valueTypeAdapter; + private final ObjectConstructor<? extends Map<K, V>> constructor; + + public Adapter(Gson context, Type keyType, TypeAdapter<K> keyTypeAdapter, + Type valueType, TypeAdapter<V> valueTypeAdapter, + ObjectConstructor<? extends Map<K, V>> constructor) { + this.keyTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<K>(context, keyTypeAdapter, keyType); + this.valueTypeAdapter = + new TypeAdapterRuntimeTypeWrapper<V>(context, valueTypeAdapter, valueType); + this.constructor = constructor; + } + + @Override + public Map<K, V> read(JsonReader in) throws IOException { + JsonToken peek = in.peek(); + if (peek == JsonToken.NULL) { + in.nextNull(); + return null; + } + + Map<K, V> map = constructor.construct(); + in.beginArray(); + while (in.hasNext()) { +// in.beginArray(); // entry array + in.beginObject(); + K key ; + V value; + String name=in.nextName(); + if(VALUE_NAME.equals(name)){ + value = valueTypeAdapter.read(in); + in.nextName(); + key = keyTypeAdapter.read(in); + }else{ + key = keyTypeAdapter.read(in); + in.nextName(); + value = valueTypeAdapter.read(in); + } + V replaced = map.put(key, value); + if (replaced != null) { + throw new JsonSyntaxException("duplicate key: " + key); + } +// in.endArray(); + in.endObject();; + } + in.endArray(); +// if (peek == JsonToken.BEGIN_ARRAY) { +// in.beginArray(); +// while (in.hasNext()) { +// in.beginArray(); // entry array +// K key = keyTypeAdapter.read(in); +// V value = valueTypeAdapter.read(in); +// V replaced = map.put(key, value); +// if (replaced != null) { +// throw new JsonSyntaxException("duplicate key: " + key); +// } +// in.endArray(); +// } +// in.endArray(); +// } else { +// in.beginObject(); +// while (in.hasNext()) { +// JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in); +// K key = keyTypeAdapter.read(in); +// V value = valueTypeAdapter.read(in); +// V replaced = map.put(key, value); +// if (replaced != null) { +// throw new JsonSyntaxException("duplicate key: " + key); +// } +// } +// in.endObject(); +// } + return map; + } + + @Override + public void write(JsonWriter out, Map<K, V> map) throws IOException { + if (map == null) { + out.nullValue(); + return; + } + + if (!complexMapKeySerialization) { + out.beginObject(); + for (Map.Entry<K, V> entry : map.entrySet()) { + out.name(String.valueOf(entry.getKey())); + valueTypeAdapter.write(out, entry.getValue()); + } + out.endObject(); + return; + } + + boolean hasComplexKeys = false; + List<JsonElement> keys = new ArrayList<JsonElement>(map.size()); + + List<V> values = new ArrayList<V>(map.size()); + for (Map.Entry<K, V> entry : map.entrySet()) { + JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey()); + keys.add(keyElement); + values.add(entry.getValue()); + hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject(); + } + + if (hasComplexKeys) { + out.beginArray(); + for (int i = 0; i < keys.size(); i++) { + out.beginArray(); // entry array + Streams.write(keys.get(i), out); + valueTypeAdapter.write(out, values.get(i)); + out.endArray(); + } + out.endArray(); + } else { + out.beginObject(); + for (int i = 0; i < keys.size(); i++) { + JsonElement keyElement = keys.get(i); + out.name(keyToString(keyElement)); + valueTypeAdapter.write(out, values.get(i)); + } + out.endObject(); + } + } + + private String keyToString(JsonElement keyElement) { + if (keyElement.isJsonPrimitive()) { + JsonPrimitive primitive = keyElement.getAsJsonPrimitive(); + if (primitive.isNumber()) { + return String.valueOf(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + return Boolean.toString(primitive.getAsBoolean()); + } else if (primitive.isString()) { + return primitive.getAsString(); + } else { + throw new AssertionError(); + } + } else if (keyElement.isJsonNull()) { + return "null"; + } else { + throw new AssertionError(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java b/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java index 6cbd6ee..0046f9a 100644 --- a/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java +++ b/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java @@ -1,171 +1,223 @@ package com.stanfy.gsonxml; import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.$Gson$Preconditions; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.internal.bind.XmlMapTypeAdapterFactory; import com.stanfy.gsonxml.XmlReader.Options; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + /** * Use this builder for constructing {@link GsonXml} object. All methods are very * similar to {@link com.google.gson.GsonBuilder}. + * * @author Roman Mazur (Stanfy - http://stanfy.com) */ public class GsonXmlBuilder { + private final Map<Type, InstanceCreator<?>> instanceCreators + = new HashMap<Type, InstanceCreator<?>>(); + /** Core builder. */ + private GsonBuilder coreBuilder; + + /** Factory for XML parser. */ + private XmlParserCreator xmlParserCreator; + + /** Options. */ + private final Options options = new Options(); + + { + // Parse option: whether to skip root element + options.skipRoot = true; + // Parse option: whether to treat XML namespaces. + options.namespaces = false; + // Parse option: list a created from a set of elements with the same name without a grouping element. + options.sameNameList = false; + } + + /** + * @param gsonBuilder instance of {@link GsonBuilder} + * @return this instance for chaining + */ + public GsonXmlBuilder wrap(final GsonBuilder gsonBuilder) { + this.coreBuilder = gsonBuilder; + initGsonBuilder(true); + return this; + } + + /** + * Set a factory for XML pull parser. + * + * @param xmlParserCreator instance of {@link XmlParserCreator} + * @return this instance for chaining + */ + public GsonXmlBuilder setXmlParserCreator(final XmlParserCreator xmlParserCreator) { + this.xmlParserCreator = xmlParserCreator; + return this; + } + + /** + * Here's the difference.<br> + * <b>Skip root: on</b> + * <pre> + * <root><name>value</name></root> + * ==> + * {name : 'value'} + * </pre> + * <b>Skip root: off</b> + * <pre> + * <root><name>value</name></root> + * ==> + * {root : {name : 'value'}} + * </pre> + * + * @param value true to skip root element + * @return this instance for chaining + */ + public GsonXmlBuilder setSkipRoot(final boolean value) { + this.options.skipRoot = value; + return this; + } + + /** + * Here's the difference.<br> + * <b>Treat namespaces: on</b> + * <pre> + * <root><ns:name>value</ns:name></root> + * ==> + * {'<ns>name' : 'value'} + * </pre> + * <b>Treat namespaces: off</b> + * <pre> + * <root><ns:name>value</ns:name></root> + * ==> + * {name : 'value'} + * </pre> + * + * @param value true to treat namespaces + * @return this instance for chaining + */ + public GsonXmlBuilder setTreatNamespaces(final boolean value) { + this.options.namespaces = value; + return this; + } + + /** + * Here's the difference.<br> + * <b>Same name lists: on</b> + * <pre> + * <root> + * <name>value</name> + * <item>value1</item> + * <item>value2</item> + * </root> + * ==> + * {name : 'value', item : ['value1', 'value2']} + * </pre> + * <b>Treat namespaces: off</b> + * <pre> + * <root> + * <name>value</name> + * <items> + * <ignored>value1</ignored> + * <ignored>value2</ignored> + * </items> + * </root> + * ==> + * {name : 'value', items : ['value1', 'value2']} + * </pre> + * + * @param value true for same name list policy + * @return this instance for chaining + */ + public GsonXmlBuilder setSameNameLists(final boolean value) { + this.options.sameNameList = value; + return this; + } + + /** + * If set to true than arrays can contain primitive values. If false only arrays can contain objects only. + * When set to true you cannot parse the next sample: + * <pre> + * <list> + * <item> + * text node value + * <field-name>field value</field-name> + * </item> + * <item>value2</item> + * </list> + * </pre> + * It's caused by the fact that parser meats 'text node value' and makes a decision that this item is primitive. + * + * @param primitiveArrays value for primitive arrays policy + * @return this instance for chaining + */ + public GsonXmlBuilder setPrimitiveArrays(final boolean primitiveArrays) { + this.options.primitiveArrays = primitiveArrays; + return this; + } + + /** + * When set to true and the root element is parsed as a collection this collection items are treated as primitives. + * + * @param rootArrayPrimitive flag for 'root array primitive' policy + * @return this instance for chaining + * @see #setPrimitiveArrays(boolean) + */ + public GsonXmlBuilder setRootArrayPrimitive(final boolean rootArrayPrimitive) { + this.options.rootArrayPrimitive = rootArrayPrimitive; + return this; + } + + /** + * < to < + * + * @return this instance for chaining + */ + public GsonXmlBuilder setDecodeString(final boolean decodeString) { + this.options.decodeString = decodeString; + return this; + } - /** Core builder. */ - private GsonBuilder coreBuilder; - - /** Factory for XML parser. */ - private XmlParserCreator xmlParserCreator; - - /** Options. */ - private final Options options = new Options(); - { - // Parse option: whether to skip root element - options.skipRoot = true; - // Parse option: whether to treat XML namespaces. - options.namespaces = false; - // Parse option: list a created from a set of elements with the same name without a grouping element. - options.sameNameList = false; - } - - /** - * @param gsonBuilder instance of {@link GsonBuilder} - * @return this instance for chaining - */ - public GsonXmlBuilder wrap(final GsonBuilder gsonBuilder) { - this.coreBuilder = gsonBuilder; - return this; - } - - /** - * Set a factory for XML pull parser. - * @param xmlParserCreator instance of {@link XmlParserCreator} - * @return this instance for chaining - */ - public GsonXmlBuilder setXmlParserCreator(final XmlParserCreator xmlParserCreator) { - this.xmlParserCreator = xmlParserCreator; - return this; - } - - /** - * Here's the difference.<br> - * <b>Skip root: on</b> - * <pre> - * <root><name>value</name></root> - * ==> - * {name : 'value'} - * </pre> - * <b>Skip root: off</b> - * <pre> - * <root><name>value</name></root> - * ==> - * {root : {name : 'value'}} - * </pre> - * @param value true to skip root element - * @return this instance for chaining - */ - public GsonXmlBuilder setSkipRoot(final boolean value) { - this.options.skipRoot = value; - return this; - } - - /** - * Here's the difference.<br> - * <b>Treat namespaces: on</b> - * <pre> - * <root><ns:name>value</ns:name></root> - * ==> - * {'<ns>name' : 'value'} - * </pre> - * <b>Treat namespaces: off</b> - * <pre> - * <root><ns:name>value</ns:name></root> - * ==> - * {name : 'value'} - * </pre> - * @param value true to treat namespaces - * @return this instance for chaining - */ - public GsonXmlBuilder setTreatNamespaces(final boolean value) { - this.options.namespaces = value; - return this; - } - - /** - * Here's the difference.<br> - * <b>Same name lists: on</b> - * <pre> - * <root> - * <name>value</name> - * <item>value1</item> - * <item>value2</item> - * </root> - * ==> - * {name : 'value', item : ['value1', 'value2']} - * </pre> - * <b>Treat namespaces: off</b> - * <pre> - * <root> - * <name>value</name> - * <items> - * <ignored>value1</ignored> - * <ignored>value2</ignored> - * </items> - * </root> - * ==> - * {name : 'value', items : ['value1', 'value2']} - * </pre> - * @param value true for same name list policy - * @return this instance for chaining - */ - public GsonXmlBuilder setSameNameLists(final boolean value) { - this.options.sameNameList = value; - return this; - } - - /** - * If set to true than arrays can contain primitive values. If false only arrays can contain objects only. - * When set to true you cannot parse the next sample: - * <pre> - * <list> - * <item> - * text node value - * <field-name>field value</field-name> - * </item> - * <item>value2</item> - * </list> - * </pre> - * It's caused by the fact that parser meats 'text node value' and makes a decision that this item is primitive. - * @param primitiveArrays value for primitive arrays policy - * @return this instance for chaining - */ - public GsonXmlBuilder setPrimitiveArrays(final boolean primitiveArrays) { - this.options.primitiveArrays = primitiveArrays; - return this; - } - - /** - * When set to true and the root element is parsed as a collection this collection items are treated as primitives. - * @see #setPrimitiveArrays(boolean) - * @param rootArrayPrimitive flag for 'root array primitive' policy - * @return this instance for chaining - */ - public GsonXmlBuilder setRootArrayPrimitive(final boolean rootArrayPrimitive) { - this.options.rootArrayPrimitive = rootArrayPrimitive; - return this; - } - - /** - * Creates a {@link GsonXml} instance based on the current configuration. This method is free of - * side-effects to this {@code GsonXmlBuilder} instance and hence can be called multiple times. - * - * @return an instance of GsonXml configured with the options currently set in this builder - */ - public GsonXml create() { - if (coreBuilder == null) { - coreBuilder = new GsonBuilder(); + /** + * Creates a {@link GsonXml} instance based on the current configuration. This method is free of + * side-effects to this {@code GsonXmlBuilder} instance and hence can be called multiple times. + * + * @return an instance of GsonXml configured with the options currently set in this builder + */ + public GsonXml create() { + initGsonBuilder(false); + return new GsonXml(coreBuilder.create(), xmlParserCreator, options); } - return new GsonXml(coreBuilder.create(), xmlParserCreator, options); - } + @SuppressWarnings({"unchecked", "rawtypes"}) + public GsonXmlBuilder registerTypeAdapter(Type type, Object typeAdapter) { + $Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?> + || typeAdapter instanceof JsonDeserializer<?> + || typeAdapter instanceof InstanceCreator<?> + || typeAdapter instanceof TypeAdapter<?>); + if (typeAdapter instanceof InstanceCreator<?>) { + instanceCreators.put(type, (InstanceCreator) typeAdapter); + } + initGsonBuilder(false); + coreBuilder.registerTypeAdapter(type, typeAdapter); + return this; + } + + private void initGsonBuilder(boolean register) { + if (coreBuilder == null) { + coreBuilder = new GsonBuilder(); + register = true; + } + if (register) { + coreBuilder.registerTypeAdapterFactory(new XmlMapTypeAdapterFactory(new ConstructorConstructor(instanceCreators), true)); + } + } } diff --git a/src/main/java/com/stanfy/gsonxml/XmlReader.java b/src/main/java/com/stanfy/gsonxml/XmlReader.java index 4f33451..e3b9e7e 100644 --- a/src/main/java/com/stanfy/gsonxml/XmlReader.java +++ b/src/main/java/com/stanfy/gsonxml/XmlReader.java @@ -3,6 +3,7 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -11,827 +12,883 @@ /** * Reads XML as JSON. + * * @author Roman Mazur (Stanfy - http://stanfy.com) */ public class XmlReader extends JsonReader { - /** Internal token type. */ - private static final int START_TAG = 1, END_TAG = 2, VALUE = 3, IGNORE = -1; - - /** Scope. */ - private static enum Scope { - /** We are inside an object. Next token should be {@link JsonToken#NAME} or {@link JsonToken#END_OBJECT}. */ - INSIDE_OBJECT(false), - /** We are inside an array. Next token should be {@link JsonToken#BEGIN_OBJECT} or {@link JsonToken#END_ARRAY}. */ - INSIDE_ARRAY(true), - /** We are inside automatically added array. Next token should be {@link JsonToken#BEGIN_OBJECT} or {@link JsonToken#END_ARRAY}. */ - INSIDE_EMBEDDED_ARRAY(true), - /** We are inside primitive embedded array. Child scope can be #PRIMITIVE_VALUE only. */ - INSIDE_PRIMITIVE_EMBEDDED_ARRAY(true), - /** We are inside primitive array. Child scope can be #PRIMITIVE_VALUE only. */ - INSIDE_PRIMITIVE_ARRAY(true), - /** We are inside primitive value. Next token should be {@link JsonToken#STRING} or {@link JsonToken#END_ARRAY}. */ - PRIMITIVE_VALUE(false), - /** New start tag met, we returned {@link JsonToken#NAME}. Object, array, or value can go next. */ - NAME(false); - - /** Inside array flag. */ - final boolean insideArray; - - private Scope(final boolean insideArray) { - this.insideArray = insideArray; - } - } - - /** XML parser. */ - private final XmlPullParser xmlParser; - - /** Option. */ - final Options options; - - /** Tokens pool. */ - private final RefsPool<TokenRef> tokensPool = new RefsPool<TokenRef>(new Creator<TokenRef>() { - public TokenRef create() { return new TokenRef(); } - }); - /** Values pool. */ - private final RefsPool<ValueRef> valuesPool = new RefsPool<ValueRef>(new Creator<ValueRef>() { - public ValueRef create() { return new ValueRef(); } - }); - - /** Tokens queue. */ - private TokenRef tokensQueue, tokensQueueStart; - /** Values queue. */ - private ValueRef valuesQueue, valuesQueueStart; - - private JsonToken expectedToken; - - /** State. */ - private boolean endReached, firstStart = true, lastTextWhiteSpace = false; - - /** Stack of scopes. */ - private final Stack<Scope> scopeStack = new Stack<Scope>(); - /** Stack of last closed tags. */ - private final Stack<ClosedTag> closeStack = new Stack<ClosedTag>(); - - /** Current token. */ - private JsonToken token; - - /** Counter for "$". */ - private int textNameCounter = 0; - - /** Skipping state flag. */ - private boolean skipping; - - /** Last XML token info. */ - private final XmlTokenInfo xmlToken = new XmlTokenInfo(); - - /** Attributes. */ - private final AttributesData attributes = new AttributesData(10); - - public XmlReader(final Reader in, final XmlParserCreator creator, final Options options) { - super(in); - this.xmlParser = creator.createParser(); - this.options = options; - this.xmlToken.type = IGNORE; - try { - this.xmlParser.setInput(in); - this.xmlParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, options.namespaces); - } catch (final XmlPullParserException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("unused") - private CharSequence dump() { - return new StringBuilder() - .append("Scopes: ").append(scopeStack).append('\n') - .append("Closed tags: ").append(closeStack).append('\n') - .append("Token: ").append(token).append('\n') - .append("Tokens queue: ").append(tokensQueueStart).append('\n') - .append("Values queue: ").append(valuesQueueStart).append('\n'); - } - - @Override - public String toString() { return "--- XmlReader ---\n" + dump(); } - - private JsonToken peekNextToken() { return tokensQueueStart != null ? tokensQueueStart.token : null; } - - private JsonToken nextToken() { - final TokenRef ref = tokensQueueStart; - if (ref == null) { - return JsonToken.END_DOCUMENT; - } - - tokensQueueStart = ref.next; - if (ref == tokensQueue) { tokensQueue = null; } - tokensPool.release(ref); - return ref.token; - } - - private ValueRef nextValue() { - final ValueRef ref = valuesQueueStart; - if (ref == null) { throw new IllegalStateException("No value can be given"); } - if (ref == valuesQueue) { valuesQueue = null; } - valuesPool.release(ref); - valuesQueueStart = ref.next; - return ref; - } - - private void expect(final JsonToken token) throws IOException { - final JsonToken actual = peek(); - this.token = null; - if (actual != token) { throw new IllegalStateException(token + " expected, but met " + actual + "\n" + dump()); } - } - - @Override - public void beginObject() throws IOException { - expectedToken = JsonToken.BEGIN_OBJECT; - expect(expectedToken); - } - @Override - public void endObject() throws IOException { - expectedToken = JsonToken.END_OBJECT; - expect(expectedToken); - } - @Override - public void beginArray() throws IOException { - expectedToken = JsonToken.BEGIN_ARRAY; - expect(expectedToken); - } - @Override - public void endArray() throws IOException { - expectedToken = JsonToken.END_ARRAY; - expect(expectedToken); - } - - @Override - public boolean hasNext() throws IOException { - peek(); - return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; - } - - @Override - public void skipValue() throws IOException { - skipping = true; - try { - int count = 0; - do { - final JsonToken token = peek(); - if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { - count++; - } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { - count--; - } else if (valuesQueue != null) { - nextValue(); // pull ignored value - } - this.token = null; // advance - } while (count != 0); - } finally { - skipping = false; - } - } - - private void adaptCurrentToken() throws XmlPullParserException, IOException { - if (token == expectedToken) { return; } - if (expectedToken != JsonToken.BEGIN_ARRAY) { return; } - - switch (token) { - - case BEGIN_OBJECT: - - token = JsonToken.BEGIN_ARRAY; - - final Scope lastScope = scopeStack.peek(); - - if (peekNextToken() == JsonToken.NAME) { - if (options.sameNameList) { - // we are replacing current scope with INSIDE_EMBEDDED_ARRAY - scopeStack.cleanup(1); + /** Internal token type. */ + private static final int START_TAG = 1, END_TAG = 2, VALUE = 3, IGNORE = -1; + + /** Scope. */ + private static enum Scope { + /** We are inside an object. Next token should be {@link JsonToken#NAME} or {@link JsonToken#END_OBJECT}. */ + INSIDE_OBJECT(false), + /** We are inside an array. Next token should be {@link JsonToken#BEGIN_OBJECT} or {@link JsonToken#END_ARRAY}. */ + INSIDE_ARRAY(true), + /** We are inside automatically added array. Next token should be {@link JsonToken#BEGIN_OBJECT} or {@link JsonToken#END_ARRAY}. */ + INSIDE_EMBEDDED_ARRAY(true), + /** We are inside primitive embedded array. Child scope can be #PRIMITIVE_VALUE only. */ + INSIDE_PRIMITIVE_EMBEDDED_ARRAY(true), + /** We are inside primitive array. Child scope can be #PRIMITIVE_VALUE only. */ + INSIDE_PRIMITIVE_ARRAY(true), + /** We are inside primitive value. Next token should be {@link JsonToken#STRING} or {@link JsonToken#END_ARRAY}. */ + PRIMITIVE_VALUE(false), + /** New start tag met, we returned {@link JsonToken#NAME}. Object, array, or value can go next. */ + NAME(false); + + /** Inside array flag. */ + final boolean insideArray; + + Scope(final boolean insideArray) { + this.insideArray = insideArray; + } + } - // use it as a field - pushToQueue(JsonToken.BEGIN_OBJECT); + /** XML parser. */ + private final XmlPullParser xmlParser; - scopeStack.push(Scope.INSIDE_EMBEDDED_ARRAY); - scopeStack.push(Scope.INSIDE_OBJECT); - if (lastScope == Scope.NAME) { - scopeStack.push(Scope.NAME); - } - } else { - // ignore name - nextToken(); - nextValue(); - - int pushPos = scopeStack.size(); - if (options.primitiveArrays && peekNextToken() == null) { - // pull what next: it can be either primitive or object - fillQueues(true); - } - pushPos = scopeStack.cleanup(3, pushPos); - - if (options.primitiveArrays && peekNextToken() == JsonToken.STRING) { - // primitive - scopeStack.pushAt(pushPos, Scope.INSIDE_PRIMITIVE_ARRAY); - } else { - // object (if array it will be adapted again) - scopeStack.pushAt(pushPos, Scope.INSIDE_ARRAY); - if (scopeStack.size() <= pushPos + 1 || scopeStack.get(pushPos + 1) != Scope.INSIDE_OBJECT) { - scopeStack.pushAt(pushPos + 1, Scope.INSIDE_OBJECT); - } - if (peekNextToken() != JsonToken.BEGIN_OBJECT) { - pushToQueue(JsonToken.BEGIN_OBJECT); - } - } + /** Option. */ + final Options options; + /** Tokens pool. */ + private final RefsPool<TokenRef> tokensPool = new RefsPool<TokenRef>(new Creator<TokenRef>() { + public TokenRef create() { + return new TokenRef(); + } + }); + /** Values pool. */ + private final RefsPool<ValueRef> valuesPool = new RefsPool<ValueRef>(new Creator<ValueRef>() { + public ValueRef create() { + return new ValueRef(); } - } - break; + }); - case STRING: - token = JsonToken.BEGIN_ARRAY; - if (options.sameNameList) { + /** Tokens queue. */ + private TokenRef tokensQueue, tokensQueueStart; + /** Values queue. */ + private ValueRef valuesQueue, valuesQueueStart; - if (options.primitiveArrays) { - // we have array of primitives - pushToQueue(JsonToken.STRING); - scopeStack.push(Scope.INSIDE_PRIMITIVE_EMBEDDED_ARRAY); - } else { - // pass value as a text node inside of an object - String value = nextValue().value; - pushToQueue(JsonToken.END_OBJECT); - pushToQueue(JsonToken.STRING); - pushToQueue(JsonToken.NAME); - pushToQueue(JsonToken.BEGIN_OBJECT); - pushToQueue(value); - pushToQueue("$"); - scopeStack.push(Scope.INSIDE_EMBEDDED_ARRAY); - } - - } else { - // we have an empty list - pushToQueue(JsonToken.END_ARRAY); - if (valuesQueueStart != null) { - nextValue(); - } - } - break; - - default: - } - - } - - @Override - public JsonToken peek() throws IOException { - if (expectedToken == null && firstStart) { return JsonToken.BEGIN_OBJECT; } - - if (token != null) { - try { - adaptCurrentToken(); - } catch (final XmlPullParserException e) { - throw new JsonSyntaxException("XML parsing exception", e); - } - expectedToken = null; - return token; - } - - try { - - fillQueues(false); - expectedToken = null; - - return token = nextToken(); - - } catch (final XmlPullParserException e) { - throw new JsonSyntaxException("XML parsing exception", e); - } - } - - @Override - public String nextString() throws IOException { - expect(JsonToken.STRING); - return GsonXml.decode(nextValue().value); - } - @Override - public boolean nextBoolean() throws IOException { - expect(JsonToken.BOOLEAN); - final String value = nextValue().value; - if ("true".equalsIgnoreCase(value)) { - return true; - } - if ("false".equalsIgnoreCase(value)) { - return true; - } - throw new IOException("Cannot parse <" + value + "> to boolean"); - } - @Override - public double nextDouble() throws IOException { - expect(JsonToken.STRING); - return Double.parseDouble(nextValue().value); - } - @Override - public int nextInt() throws IOException { - expect(JsonToken.STRING); - return Integer.parseInt(nextValue().value); - } - @Override - public long nextLong() throws IOException { - expect(JsonToken.STRING); - return Long.parseLong(nextValue().value); - } - @Override - public String nextName() throws IOException { - expectedToken = JsonToken.NAME; - expect(JsonToken.NAME); - return nextValue().value; - } - - - private XmlTokenInfo nextXmlInfo() throws IOException, XmlPullParserException { - final int type = xmlParser.next(); - - final XmlTokenInfo info = this.xmlToken; - info.clear(); - - switch (type) { - - case XmlPullParser.START_TAG: - info.type = START_TAG; - info.name = xmlParser.getName(); - info.ns = xmlParser.getNamespace(); - final int aCount = xmlParser.getAttributeCount(); - if (aCount > 0) { - attributes.fill(xmlParser); - info.attributesData = attributes; - } - break; - - case XmlPullParser.END_TAG: - info.type = END_TAG; - info.name = xmlParser.getName(); - info.ns = xmlParser.getNamespace(); - break; - - case XmlPullParser.TEXT: - final String text = xmlParser.getText().trim(); - if (text.length() == 0) { - lastTextWhiteSpace = true; - info.type = IGNORE; - return info; - } - lastTextWhiteSpace = false; - info.type = VALUE; - info.value = text; - break; - - - case XmlPullParser.END_DOCUMENT: - endReached = true; - // fall through - - default: - info.type = IGNORE; - } - - return info; - } - - private void addToQueue(final JsonToken token) { - final TokenRef tokenRef = tokensPool.get(); - tokenRef.token = token; - tokenRef.next = null; - - if (tokensQueue == null) { - tokensQueue = tokenRef; - tokensQueueStart = tokenRef; - } else { - tokensQueue.next = tokenRef; - tokensQueue = tokenRef; - } - } - private void pushToQueue(final JsonToken token) { - final TokenRef tokenRef = tokensPool.get(); - tokenRef.token = token; - tokenRef.next = null; - - if (tokensQueueStart == null) { - tokensQueueStart = tokenRef; - tokensQueue = tokenRef; - } else { - tokenRef.next = tokensQueueStart; - tokensQueueStart = tokenRef; - } - } - private void addToQueue(final String value) { - final ValueRef valueRef = valuesPool.get(); - valueRef.value = value.trim(); - valueRef.next = null; - - if (valuesQueue == null) { - valuesQueue = valueRef; - valuesQueueStart = valueRef; - } else { - valuesQueue.next = valueRef; - valuesQueue = valueRef; - } - } - private void pushToQueue(final String value) { - final ValueRef valueRef = valuesPool.get(); - valueRef.value = value; - valueRef.next = null; - - if (valuesQueueStart == null) { - valuesQueue = valueRef; - valuesQueueStart = valueRef; - } else { - valueRef.next = valuesQueueStart; - valuesQueueStart = valueRef; - } - } - private void addToQueue(final AttributesData attrData) throws IOException, XmlPullParserException { - final int count = attrData.count; - for (int i = 0; i < count; i++) { - addToQueue(JsonToken.NAME); - addToQueue("@" + attrData.getName(i)); - addToQueue(JsonToken.STRING); - addToQueue(attrData.values[i]); - } - } - - private void fillQueues(boolean force) throws IOException, XmlPullParserException { - - boolean mustRepeat = force; - - while ((tokensQueue == null && !endReached) || mustRepeat) { - final XmlTokenInfo xml = nextXmlInfo(); - if (endReached) { - if (!options.skipRoot) { addToQueue(JsonToken.END_OBJECT); } - break; - } - if (xml.type == IGNORE) { continue; } - - mustRepeat = false; - - switch (xml.type) { - case START_TAG: - if (firstStart) { - firstStart = false; - processRoot(xml); - } else { - processStart(xml); + private JsonToken expectedToken; + + /** State. */ + private boolean endReached, firstStart = true, lastTextWhiteSpace = false; + + /** Stack of scopes. */ + private final Stack<Scope> scopeStack = new Stack<Scope>(); + /** Stack of last closed tags. */ + private final Stack<ClosedTag> closeStack = new Stack<ClosedTag>(); + + /** Current token. */ + private JsonToken token; + + /** Counter for "$". */ + private int textNameCounter = 0; + + /** Skipping state flag. */ + private boolean skipping; + + /** Last XML token info. */ + private final XmlTokenInfo xmlToken = new XmlTokenInfo(); + + /** Attributes. */ + private final AttributesData attributes = new AttributesData(10); + + public XmlReader(final Reader in, final XmlParserCreator creator, final Options options) { + super(in); + this.xmlParser = creator.createParser(); + this.options = options; + this.xmlToken.type = IGNORE; + try { + this.xmlParser.setInput(in); + this.xmlParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, options.namespaces); + } catch (final XmlPullParserException e) { + throw new RuntimeException(e); } - break; - case VALUE: - mustRepeat = processText(xml); - break; - case END_TAG: - processEnd(xml); - break; - default: - } + } + + @SuppressWarnings("unused") + private CharSequence dump() { + return new StringBuilder() + .append("Scopes: ").append(scopeStack).append('\n') + .append("Closed tags: ").append(closeStack).append('\n') + .append("Token: ").append(token).append('\n') + .append("Tokens queue: ").append(tokensQueueStart).append('\n') + .append("Values queue: ").append(valuesQueueStart).append('\n'); + } + + @Override + public String toString() { + return "--- XmlReader ---\n" + dump(); + } - if (!mustRepeat && skipping) { break; } + private JsonToken peekNextToken() { + return tokensQueueStart != null ? tokensQueueStart.token : null; } - } - private void processRoot(final XmlTokenInfo xml) throws IOException, XmlPullParserException { - if (!options.skipRoot) { + private JsonToken nextToken() { + final TokenRef ref = tokensQueueStart; + if (ref == null) { + return JsonToken.END_DOCUMENT; + } - addToQueue(expectedToken); - scopeStack.push(Scope.INSIDE_OBJECT); - processStart(xml); + tokensQueueStart = ref.next; + if (ref == tokensQueue) { + tokensQueue = null; + } + tokensPool.release(ref); + return ref.token; + } - } else if (xml.attributesData != null) { + private ValueRef nextValue() { + final ValueRef ref = valuesQueueStart; + if (ref == null) { + throw new IllegalStateException("No value can be given"); + } + if (ref == valuesQueue) { + valuesQueue = null; + } + valuesPool.release(ref); + valuesQueueStart = ref.next; + return ref; + } - addToQueue(JsonToken.BEGIN_OBJECT); - scopeStack.push(Scope.INSIDE_OBJECT); - addToQueue(xml.attributesData); + private void expect(final JsonToken token) throws IOException { + final JsonToken actual = peek(); + this.token = null; + if (actual != token) { + throw new IllegalStateException(token + " expected, but met " + actual + "\n" + dump()); + } + } - } else { + @Override + public void beginObject() throws IOException { + expectedToken = JsonToken.BEGIN_OBJECT; + expect(expectedToken); + } - switch (expectedToken) { - case BEGIN_OBJECT: - addToQueue(JsonToken.BEGIN_OBJECT); - scopeStack.push(Scope.INSIDE_OBJECT); - break; - case BEGIN_ARRAY: - addToQueue(JsonToken.BEGIN_ARRAY); - scopeStack.push(options.rootArrayPrimitive ? Scope.INSIDE_PRIMITIVE_ARRAY : Scope.INSIDE_ARRAY); - break; - default: - throw new IllegalStateException("First expectedToken=" + expectedToken + " (not begin_object/begin_array)"); - } + @Override + public void endObject() throws IOException { + expectedToken = JsonToken.END_OBJECT; + expect(expectedToken); + } + + @Override + public void beginArray() throws IOException { + expectedToken = JsonToken.BEGIN_ARRAY; + expect(expectedToken); + } + @Override + public void endArray() throws IOException { + expectedToken = JsonToken.END_ARRAY; + expect(expectedToken); } - } - private void processStart(final XmlTokenInfo xml) throws IOException, XmlPullParserException { + @Override + public boolean hasNext() throws IOException { + peek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } - boolean processTagName = true; + @Override + public void skipValue() throws IOException { + skipping = true; + try { + int count = 0; + do { + final JsonToken token = peek(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } else if (valuesQueue != null) { + nextValue(); // pull ignored value + } + this.token = null; // advance + } while (count != 0); + } finally { + skipping = false; + } + } - Scope lastScope = scopeStack.peek(); + private void adaptCurrentToken() throws XmlPullParserException, IOException { + if (token == expectedToken) { + return; + } + if (expectedToken != JsonToken.BEGIN_ARRAY) { + return; + } - if (options.sameNameList && lastScope.insideArray && closeStack.size() > 0) { - ClosedTag lastClosedInfo = closeStack.peek(); - if (lastClosedInfo.depth == xmlParser.getDepth()) { - String currentName = options.namespaces ? xml.getName(xmlParser) : xml.name; - if (!currentName.equals(lastClosedInfo.name)) { - // close the previous array - addToQueue(JsonToken.END_ARRAY); - fixScopeStack(); - lastScope = scopeStack.peek(); + switch (token) { + + case BEGIN_OBJECT: + + token = JsonToken.BEGIN_ARRAY; + + final Scope lastScope = scopeStack.peek(); + + if (peekNextToken() == JsonToken.NAME) { + if (options.sameNameList) { + // we are replacing current scope with INSIDE_EMBEDDED_ARRAY + scopeStack.cleanup(1); + + // use it as a field + pushToQueue(JsonToken.BEGIN_OBJECT); + + scopeStack.push(Scope.INSIDE_EMBEDDED_ARRAY); + scopeStack.push(Scope.INSIDE_OBJECT); + if (lastScope == Scope.NAME) { + scopeStack.push(Scope.NAME); + } + } else { + // ignore name + nextToken(); + nextValue(); + + int pushPos = scopeStack.size(); + if (options.primitiveArrays && peekNextToken() == null) { + // pull what next: it can be either primitive or object + fillQueues(true); + } + pushPos = scopeStack.cleanup(3, pushPos); + + if (options.primitiveArrays && peekNextToken() == JsonToken.STRING) { + // primitive + scopeStack.pushAt(pushPos, Scope.INSIDE_PRIMITIVE_ARRAY); + } else { + // object (if array it will be adapted again) + scopeStack.pushAt(pushPos, Scope.INSIDE_ARRAY); + if (scopeStack.size() <= pushPos + 1 || scopeStack.get(pushPos + 1) != Scope.INSIDE_OBJECT) { + scopeStack.pushAt(pushPos + 1, Scope.INSIDE_OBJECT); + } + if (peekNextToken() != JsonToken.BEGIN_OBJECT) { + pushToQueue(JsonToken.BEGIN_OBJECT); + } + } + + } + } + break; + + case STRING: + token = JsonToken.BEGIN_ARRAY; + if (options.sameNameList) { + + if (options.primitiveArrays) { + // we have array of primitives + pushToQueue(JsonToken.STRING); + scopeStack.push(Scope.INSIDE_PRIMITIVE_EMBEDDED_ARRAY); + } else { + // pass value as a text node inside of an object + String value = nextValue().value; + pushToQueue(JsonToken.END_OBJECT); + pushToQueue(JsonToken.STRING); + pushToQueue(JsonToken.NAME); + pushToQueue(JsonToken.BEGIN_OBJECT); + pushToQueue(value); + pushToQueue("$"); + scopeStack.push(Scope.INSIDE_EMBEDDED_ARRAY); + } + + } else { + // we have an empty list + pushToQueue(JsonToken.END_ARRAY); + if (valuesQueueStart != null) { + nextValue(); + } + } + break; + + default: } - } - } - - switch (lastScope) { - - case INSIDE_PRIMITIVE_ARRAY: - case INSIDE_PRIMITIVE_EMBEDDED_ARRAY: - processTagName = false; - scopeStack.push(Scope.PRIMITIVE_VALUE); - break; - - case INSIDE_EMBEDDED_ARRAY: - case INSIDE_ARRAY: - processTagName = false; - // fall through - - case NAME: - addToQueue(JsonToken.BEGIN_OBJECT); - scopeStack.push(Scope.INSIDE_OBJECT); - break; - - default: - } - - if (processTagName) { // ignore tag name inside the array - scopeStack.push(Scope.NAME); - addToQueue(JsonToken.NAME); - addToQueue(xml.getName(xmlParser)); - lastTextWhiteSpace = true; // if tag is closed immediately we'll add empty value to the queue - } - - if (xml.attributesData != null) { - lastScope = scopeStack.peek(); - if (lastScope == Scope.PRIMITIVE_VALUE) { throw new IllegalStateException("Attributes data in primitive scope"); } - if (lastScope == Scope.NAME) { - addToQueue(JsonToken.BEGIN_OBJECT); - scopeStack.push(Scope.INSIDE_OBJECT); - } - // attributes, as fields - addToQueue(xml.attributesData); - } - } - - private boolean processText(final XmlTokenInfo xml) { - switch (scopeStack.peek()) { - - case PRIMITIVE_VALUE: - addTextToQueue(xml.value, false); - return false; - - case NAME: - addTextToQueue(xml.value, true); - return true; - - case INSIDE_OBJECT: - String name = "$"; - if (textNameCounter > 0) { name += textNameCounter; } - textNameCounter++; - addToQueue(JsonToken.NAME); - addToQueue(name); - addTextToQueue(xml.value, false); - return false; - - default: - throw new JsonSyntaxException("Cannot process text '" + xml.value + "' inside scope " + scopeStack.peek()); - } - } - - private void addTextToQueue(final String value, final boolean canBeAppended) { - if (canBeAppended && tokensQueue != null && tokensQueue.token == JsonToken.STRING) { - if (value.length() > 0) { - valuesQueue.value += " " + value; - } - } else { - addToQueue(JsonToken.STRING); - addToQueue(value); - } - } - - private void fixScopeStack() { - scopeStack.fix(Scope.NAME); - } - - private void processEnd(final XmlTokenInfo xml) throws IOException, XmlPullParserException { - switch (scopeStack.peek()) { - - case INSIDE_OBJECT: - addToQueue(JsonToken.END_OBJECT); - textNameCounter = 0; - fixScopeStack(); - break; - - case PRIMITIVE_VALUE: - scopeStack.drop(); - break; - - case INSIDE_PRIMITIVE_EMBEDDED_ARRAY: - case INSIDE_EMBEDDED_ARRAY: - addToQueue(JsonToken.END_ARRAY); - addToQueue(JsonToken.END_OBJECT); - fixScopeStack(); // auto-close embedded array - fixScopeStack(); // close current object scope - break; - - case INSIDE_PRIMITIVE_ARRAY: - case INSIDE_ARRAY: - addToQueue(JsonToken.END_ARRAY); - fixScopeStack(); - break; - - case NAME: - if (lastTextWhiteSpace) { - addTextToQueue("", true); - } - fixScopeStack(); - break; - - default: - // nothing - } - - if (options.sameNameList) { - int stackSize = xmlParser.getDepth(); - final String name = options.namespaces ? xml.getName(xmlParser) : xml.name; - final Stack<ClosedTag> closeStack = this.closeStack; - boolean nameChange = false; - while (closeStack.size() > 0 && closeStack.peek().depth > stackSize) { - closeStack.drop(); - } - if (closeStack.size() == 0 || closeStack.peek().depth < stackSize) { - closeStack.push(new ClosedTag(stackSize, name)); - } else { - closeStack.peek().name = name; - } - } - } - - private static final class TokenRef { - JsonToken token; - TokenRef next; + + } + @Override - public String toString() { - return token + ", " + next; + public JsonToken peek() throws IOException { + if (expectedToken == null && firstStart) { + return JsonToken.BEGIN_OBJECT; + } + + if (token != null) { + try { + adaptCurrentToken(); + } catch (final XmlPullParserException e) { + throw new JsonSyntaxException("XML parsing exception", e); + } + expectedToken = null; + return token; + } + + try { + + fillQueues(false); + expectedToken = null; + + return token = nextToken(); + + } catch (final XmlPullParserException e) { + throw new JsonSyntaxException("XML parsing exception", e); + } } - } - private static final class ValueRef { - String value; - ValueRef next; + @Override - public String toString() { - return value + ", " + next; + public String nextString() throws IOException { + expect(JsonToken.STRING); + if (options.decodeString) + return GsonXml.decode(nextValue().value); + return nextValue().value; } - } - static String nameWithNs(final String name, final String namespace, final XmlPullParser parser) throws XmlPullParserException { - String result = name; - String ns = namespace; - if (ns != null && ns.length() > 0) { - if (parser != null) { - final int count = parser.getNamespaceCount(parser.getDepth()); - for (int i = 0; i < count; i++) { - if (ns.equals(parser.getNamespaceUri(i))) { - ns = parser.getNamespacePrefix(i); - break; - } + @Override + public boolean nextBoolean() throws IOException { + expect(JsonToken.BOOLEAN); + final String value = nextValue().value; + if ("true".equalsIgnoreCase(value)) { + return true; + } + if ("false".equalsIgnoreCase(value)) { + return true; } - } - result = "<" + ns + ">" + result; + throw new IOException("Cannot parse <" + value + "> to boolean"); } - return result; - } - private static final class XmlTokenInfo { - int type; - String name, value, ns; + @Override + public double nextDouble() throws IOException { + expect(JsonToken.STRING); + return Double.parseDouble(nextValue().value); + } - AttributesData attributesData; + @Override + public int nextInt() throws IOException { + expect(JsonToken.STRING); + return Integer.parseInt(nextValue().value); + } - public void clear() { - type = IGNORE; - name = null; - value = null; - ns = null; - attributesData = null; + @Override + public long nextLong() throws IOException { + expect(JsonToken.STRING); + return Long.parseLong(nextValue().value); } @Override - public String toString() { - return "xml " - + (type == START_TAG ? "start" : type == END_TAG ? "end" : "value") - + " <" + ns + ":" + name + ">=" + value + (attributesData != null ? ", " + attributesData : ""); + public String nextName() throws IOException { + expectedToken = JsonToken.NAME; + expect(JsonToken.NAME); + return nextValue().value; + } + + + private XmlTokenInfo nextXmlInfo() throws IOException, XmlPullParserException { + final int type = xmlParser.next(); + + final XmlTokenInfo info = this.xmlToken; + info.clear(); + + switch (type) { + + case XmlPullParser.START_TAG: + info.type = START_TAG; + info.name = xmlParser.getName(); + info.ns = xmlParser.getNamespace(); + final int aCount = xmlParser.getAttributeCount(); + if (aCount > 0) { + attributes.fill(xmlParser); + info.attributesData = attributes; + } + break; + + case XmlPullParser.END_TAG: + info.type = END_TAG; + info.name = xmlParser.getName(); + info.ns = xmlParser.getNamespace(); + break; + + case XmlPullParser.TEXT: + final String text = xmlParser.getText().trim(); + if (text.length() == 0) { + lastTextWhiteSpace = true; + info.type = IGNORE; + return info; + } + lastTextWhiteSpace = false; + info.type = VALUE; + info.value = text; + break; + + + case XmlPullParser.END_DOCUMENT: + endReached = true; + // fall through + + default: + info.type = IGNORE; + } + + return info; + } + + private void addToQueue(final JsonToken token) { + final TokenRef tokenRef = tokensPool.get(); + tokenRef.token = token; + tokenRef.next = null; + + if (tokensQueue == null) { + tokensQueue = tokenRef; + tokensQueueStart = tokenRef; + } else { + tokensQueue.next = tokenRef; + tokensQueue = tokenRef; + } + } + + private void pushToQueue(final JsonToken token) { + final TokenRef tokenRef = tokensPool.get(); + tokenRef.token = token; + tokenRef.next = null; + + if (tokensQueueStart == null) { + tokensQueueStart = tokenRef; + tokensQueue = tokenRef; + } else { + tokenRef.next = tokensQueueStart; + tokensQueueStart = tokenRef; + } + } + + private void addToQueue(final String value) { + final ValueRef valueRef = valuesPool.get(); + valueRef.value = value.trim(); + valueRef.next = null; + + if (valuesQueue == null) { + valuesQueue = valueRef; + valuesQueueStart = valueRef; + } else { + valuesQueue.next = valueRef; + valuesQueue = valueRef; + } + } + + private void pushToQueue(final String value) { + final ValueRef valueRef = valuesPool.get(); + valueRef.value = value; + valueRef.next = null; + + if (valuesQueueStart == null) { + valuesQueue = valueRef; + valuesQueueStart = valueRef; + } else { + valueRef.next = valuesQueueStart; + valuesQueueStart = valueRef; + } + } + + private void addToQueue(final AttributesData attrData) throws IOException, XmlPullParserException { + final int count = attrData.count; + for (int i = 0; i < count; i++) { + addToQueue(JsonToken.NAME); + addToQueue("@" + attrData.getName(i)); + addToQueue(JsonToken.STRING); + addToQueue(attrData.values[i]); + } } - public String getName(final XmlPullParser parser) throws IOException, XmlPullParserException { - return nameWithNs(name, ns, parser); + private void fillQueues(boolean force) throws IOException, XmlPullParserException { + + boolean mustRepeat = force; + + while ((tokensQueue == null && !endReached) || mustRepeat) { + final XmlTokenInfo xml = nextXmlInfo(); + if (endReached) { + if (!options.skipRoot) { + addToQueue(JsonToken.END_OBJECT); + } + break; + } + if (xml.type == IGNORE) { + continue; + } + + mustRepeat = false; + + switch (xml.type) { + case START_TAG: + if (firstStart) { + firstStart = false; + processRoot(xml); + } else { + processStart(xml); + } + break; + case VALUE: + mustRepeat = processText(xml); + break; + case END_TAG: + processEnd(xml); + break; + default: + } + + if (!mustRepeat && skipping) { + break; + } + } } - } - private final class AttributesData { - String[] names, values, ns; + private void processRoot(final XmlTokenInfo xml) throws IOException, XmlPullParserException { + if (!options.skipRoot) { - int count = 0; + addToQueue(expectedToken); + scopeStack.push(Scope.INSIDE_OBJECT); + processStart(xml); - public AttributesData(final int capacity) { - createArrays(capacity); + } else if (xml.attributesData != null) { + + addToQueue(JsonToken.BEGIN_OBJECT); + scopeStack.push(Scope.INSIDE_OBJECT); + addToQueue(xml.attributesData); + + } else { + + switch (expectedToken) { + case BEGIN_OBJECT: + addToQueue(JsonToken.BEGIN_OBJECT); + scopeStack.push(Scope.INSIDE_OBJECT); + break; + case BEGIN_ARRAY: + addToQueue(JsonToken.BEGIN_ARRAY); + scopeStack.push(options.rootArrayPrimitive ? Scope.INSIDE_PRIMITIVE_ARRAY : Scope.INSIDE_ARRAY); + break; + default: + throw new IllegalStateException("First expectedToken=" + expectedToken + " (not begin_object/begin_array)"); + } + + } } - private void createArrays(final int capacity) { - this.names = new String[capacity]; - this.values = new String[capacity]; - this.ns = new String[capacity]; + private void processStart(final XmlTokenInfo xml) throws IOException, XmlPullParserException { + + boolean processTagName = true; + + Scope lastScope = scopeStack.peek(); + + if (options.sameNameList && lastScope.insideArray && closeStack.size() > 0) { + ClosedTag lastClosedInfo = closeStack.peek(); + if (lastClosedInfo.depth == xmlParser.getDepth()) { + String currentName = options.namespaces ? xml.getName(xmlParser) : xml.name; + if (!currentName.equals(lastClosedInfo.name)) { + // close the previous array + addToQueue(JsonToken.END_ARRAY); + fixScopeStack(); + lastScope = scopeStack.peek(); + } + } + } + + switch (lastScope) { + + case INSIDE_PRIMITIVE_ARRAY: + case INSIDE_PRIMITIVE_EMBEDDED_ARRAY: + processTagName = false; + scopeStack.push(Scope.PRIMITIVE_VALUE); + break; + + case INSIDE_EMBEDDED_ARRAY: + case INSIDE_ARRAY: + processTagName = false; + // fall through + + case NAME: + addToQueue(JsonToken.BEGIN_OBJECT); + scopeStack.push(Scope.INSIDE_OBJECT); + break; + + default: + } + + if (processTagName) { // ignore tag name inside the array + scopeStack.push(Scope.NAME); + addToQueue(JsonToken.NAME); + addToQueue(xml.getName(xmlParser)); + lastTextWhiteSpace = true; // if tag is closed immediately we'll add empty value to the queue + } + + if (xml.attributesData != null) { + lastScope = scopeStack.peek(); + if (lastScope == Scope.PRIMITIVE_VALUE) { + throw new IllegalStateException("Attributes data in primitive scope"); + } + if (lastScope == Scope.NAME) { + addToQueue(JsonToken.BEGIN_OBJECT); + scopeStack.push(Scope.INSIDE_OBJECT); + } + // attributes, as fields + addToQueue(xml.attributesData); + } } - public void fill(final XmlPullParser parser) { - final int aCount = parser.getAttributeCount(); - if (aCount > names.length) { - createArrays(aCount); - } + private boolean processText(final XmlTokenInfo xml) { + switch (scopeStack.peek()) { + + case PRIMITIVE_VALUE: + addTextToQueue(xml.value, false); + return false; + + case NAME: + addTextToQueue(xml.value, true); + return true; + + case INSIDE_OBJECT: + String name = "$"; + if (textNameCounter > 0) { + name += textNameCounter; + } + textNameCounter++; + addToQueue(JsonToken.NAME); + addToQueue(name); + addTextToQueue(xml.value, false); + return false; - count = aCount; - for (int i = 0; i < aCount; i++) { - names[i] = parser.getAttributeName(i); - if (options.namespaces) { - ns[i] = parser.getAttributePrefix(i); + default: + throw new JsonSyntaxException("Cannot process text '" + xml.value + "' inside scope " + scopeStack.peek()); } - values[i] = parser.getAttributeValue(i); - } } - public String getName(final int i) throws IOException, XmlPullParserException { - return nameWithNs(names[i], ns[i], null); + private void addTextToQueue(final String value, final boolean canBeAppended) { + if (canBeAppended && tokensQueue != null && tokensQueue.token == JsonToken.STRING) { + if (value.length() > 0) { + valuesQueue.value += " " + value; + } + } else { + addToQueue(JsonToken.STRING); + addToQueue(value); + } + } + + private void fixScopeStack() { + scopeStack.fix(Scope.NAME); + } + + private void processEnd(final XmlTokenInfo xml) throws IOException, XmlPullParserException { + switch (scopeStack.peek()) { + + case INSIDE_OBJECT: + addToQueue(JsonToken.END_OBJECT); + textNameCounter = 0; + fixScopeStack(); + break; + + case PRIMITIVE_VALUE: + scopeStack.drop(); + break; + + case INSIDE_PRIMITIVE_EMBEDDED_ARRAY: + case INSIDE_EMBEDDED_ARRAY: + addToQueue(JsonToken.END_ARRAY); + addToQueue(JsonToken.END_OBJECT); + fixScopeStack(); // auto-close embedded array + fixScopeStack(); // close current object scope + break; + + case INSIDE_PRIMITIVE_ARRAY: + case INSIDE_ARRAY: + addToQueue(JsonToken.END_ARRAY); + fixScopeStack(); + break; + + case NAME: + if (lastTextWhiteSpace) { + addTextToQueue("", true); + } + fixScopeStack(); + break; + + default: + // nothing + break; + } + + if (options.sameNameList) { + int stackSize = xmlParser.getDepth(); + final String name = options.namespaces ? xml.getName(xmlParser) : xml.name; + final Stack<ClosedTag> closeStack = this.closeStack; +// boolean nameChange = false; + while (closeStack.size() > 0 && closeStack.peek().depth > stackSize) { + closeStack.drop(); + } + if (closeStack.size() == 0 || closeStack.peek().depth < stackSize) { + closeStack.push(new ClosedTag(stackSize, name)); + } else { + closeStack.peek().name = name; + } + } } - } + private static final class TokenRef { + JsonToken token; + TokenRef next; - /** Xml reader options. */ - public static class Options { - /** Options. */ - boolean primitiveArrays, skipRoot, sameNameList, namespaces, rootArrayPrimitive; - } + @Override + public String toString() { + return token + ", " + next; + } + } - /** Closed tag data. */ - private static class ClosedTag { - int depth; - String name; + private static final class ValueRef { + String value; + ValueRef next; - public ClosedTag(final int depth, final String name) { - this.depth = depth; - this.name = name; + @Override + public String toString() { + return value + ", " + next; + } } - @Override - public String toString() { - return "'" + name + "'/" + depth; + static String nameWithNs(final String name, final String namespace, final XmlPullParser parser) throws XmlPullParserException { + String result = name; + String ns = namespace; + if (ns != null && ns.length() > 0) { + if (parser != null) { + final int count = parser.getNamespaceCount(parser.getDepth()); + for (int i = 0; i < count; i++) { + if (ns.equals(parser.getNamespaceUri(i))) { + ns = parser.getNamespacePrefix(i); + break; + } + } + } + result = "<" + ns + ">" + result; + } + return result; + } + + private static final class XmlTokenInfo { + int type; + String name, value, ns; + + AttributesData attributesData; + + public void clear() { + type = IGNORE; + name = null; + value = null; + ns = null; + attributesData = null; + } + + @Override + public String toString() { + return "xml " + + (type == START_TAG ? "start" : type == END_TAG ? "end" : "value") + + " <" + ns + ":" + name + ">=" + value + (attributesData != null ? ", " + attributesData : ""); + } + + public String getName(final XmlPullParser parser) throws IOException, XmlPullParserException { + return nameWithNs(name, ns, parser); + } } - } - /** Pool for */ - private static final class RefsPool<T> { + private final class AttributesData { + String[] names, values, ns; + + int count = 0; + + public AttributesData(final int capacity) { + createArrays(capacity); + } - /** Max count. */ - private static final int SIZE = 32; + private void createArrays(final int capacity) { + this.names = new String[capacity]; + this.values = new String[capacity]; + this.ns = new String[capacity]; + } - /** Factory instance. */ - private final Creator<T> creator; + public void fill(final XmlPullParser parser) { + final int aCount = parser.getAttributeCount(); + if (aCount > names.length) { + createArrays(aCount); + } - /** Pool. */ - private final Object[] store = new Object[SIZE]; + count = aCount; + for (int i = 0; i < aCount; i++) { + names[i] = parser.getAttributeName(i); + if (options.namespaces) { + ns[i] = parser.getAttributePrefix(i); + } + values[i] = parser.getAttributeValue(i); + } + } - /** Store length. */ - private int len = 0; + public String getName(final int i) throws IOException, XmlPullParserException { + return nameWithNs(names[i], ns[i], null); + } - public RefsPool(final Creator<T> factory) { - this.creator = factory; } - /** Get value from pool or create it. */ - public T get() { - if (len == 0) { return creator.create(); } - return (T)store[--len]; + /** Xml reader options. */ + public static class Options { + /** Options. */ + boolean primitiveArrays, skipRoot, sameNameList, namespaces, rootArrayPrimitive, decodeString; } - /** Return value to the pool. */ - public void release(final T obj) { - if (len < SIZE) { - store[len++] = obj; - } + /** Closed tag data. */ + private static class ClosedTag { + int depth; + String name; + + public ClosedTag(final int depth, final String name) { + this.depth = depth; + this.name = name; + } + + @Override + public String toString() { + return "'" + name + "'/" + depth; + } } - } + /** Pool for */ + private static final class RefsPool<T> { + + /** Max count. */ + private static final int SIZE = 32; + + /** Factory instance. */ + private final Creator<T> creator; + + /** Pool. */ + private final Object[] store = new Object[SIZE]; - /** Factory. */ - private interface Creator<T> { T create(); } + /** Store length. */ + private int len = 0; + + public RefsPool(final Creator<T> factory) { + this.creator = factory; + } + + /** Get value from pool or create it. */ + @SuppressWarnings("unchecked") + public T get() { + if (len == 0) { + return creator.create(); + } + return (T) store[--len]; + } + + /** Return value to the pool. */ + public void release(final T obj) { + if (len < SIZE) { + store[len++] = obj; + } + } + + } + + /** Factory. */ + private interface Creator<T> { + T create(); + } } diff --git a/src/test/java/com/stanfy/gsonxml/test/DemoTest.java b/src/test/java/com/stanfy/gsonxml/test/DemoTest.java new file mode 100644 index 0000000..6693e0b --- /dev/null +++ b/src/test/java/com/stanfy/gsonxml/test/DemoTest.java @@ -0,0 +1,175 @@ +package com.stanfy.gsonxml.test; + +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.stanfy.gsonxml.GsonXmlBuilder; + +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class DemoTest extends AbstractXmlTest { + + public static class A { + public A() { + maps = new LinkedHashMap<String, B>(); + } + + @SerializedName("@id") + long id; + @SerializedName("name") + List<String> names; + @SerializedName("b") + List<B> bs; + + @SerializedName("id") + int[] is; + @SerializedName(value = "map") + Map<String, B> maps; + //List<MyMap> maps; + + @SerializedName("type") + C c; + } + + public static class MyMap { + @SerializedName("key") + String key; + @SerializedName("value") + B value; + } + + public static class B { + @SerializedName("@id") + long id; + @SerializedName("$") + String text; + + @SerializedName("@type") + C c; + } + + public enum C { + a, b, c + } + + final static String TEST_XML = + " <a id=\"123\">" + + " <name>test name1</name>" + + " <name>test name2</name>" + + " <b id=\"1234567890\">b name1</b>" + + " <b id=\"0123\">b name2</b>" + + " <map>" + + " <key>hello</key>" + + " <value id=\"1\">world</value>" + + " </map>" + + " <map>" + + " <value id=\"1\" type=\"a\" >name</value>" + + " <key>hello2</key>" + + " </map>" + + " <id>1</id>" + + " <id>2</id>" + + " <id>3</id>" + + " <type>b</type>" + + "</a>"; + final static String TEST_XML_MAP = + " <a>" + + " <map>" + + " <key>hello</key>" + + " <value id=\"1\">world</value>" + + " </map>" + + " <map>" + + " <value id=\"1\" type=\"a\" >name</value>" + + " <key>hello2</key>" + + " </map>" + + "</a>"; + + @Test + public void test() { + GsonBuilder builder = new GsonBuilder(); + builder.enableComplexMapKeySerialization(); + builder.registerTypeAdapter(C.class, new TypeAdapter<C>() { + @Override + public C read(JsonReader in) throws IOException { + String value = in.nextString(); + C[] cs = C.values(); + for (C c : cs) { + if (c.toString().equalsIgnoreCase(value)) { + return c; + } + } + return null; + } + + @Override + public void write(JsonWriter out, C value) throws IOException { + out.value(value == null ? "" : value.toString()); + } + }); + final A a = new GsonXmlBuilder() + .setXmlParserCreator(SimpleXmlReaderTest.PARSER_CREATOR) + .wrap(builder) + .setPrimitiveArrays(true) + .setSameNameLists(true) + .create() + .fromXml(TEST_XML, A.class); + assertEquals("test name2", a.names.get(1)); + assertEquals(1234567890, a.bs.get(0).id); + assertEquals("b name2", a.bs.get(1).text); + assertEquals(2, a.bs.size()); + assertEquals(C.b, a.c); + assertEquals("world", a.maps.get("hello").text); + assertEquals("name", a.maps.get("hello2").text); + assertEquals(C.a, a.maps.get("hello2").c); + assertEquals(2, a.maps.size()); + assertEquals(3, a.is.length); + assertEquals(LinkedHashMap.class, a.maps.getClass()); +// assertEquals(2, a.maps.get(1).value.id); + } + + @Test + public void testRootMap() { + GsonBuilder builder = new GsonBuilder(); + builder.enableComplexMapKeySerialization(); + builder.registerTypeAdapter(C.class, new TypeAdapter<C>() { + @Override + public C read(JsonReader in) throws IOException { + String value = in.nextString(); + C[] cs = C.values(); + for (C c : cs) { + if (c.toString().equalsIgnoreCase(value)) { + return c; + } + } + return null; + } + + @Override + public void write(JsonWriter out, C value) throws IOException { + out.value(value == null ? "" : value.toString()); + } + }); + Map<String, B> maps = new GsonXmlBuilder() + .setXmlParserCreator(SimpleXmlReaderTest.PARSER_CREATOR) + .wrap(builder) + .setSkipRoot(true) + .setPrimitiveArrays(true) + .setSameNameLists(true) + .create() + .fromXml(TEST_XML_MAP, new TypeToken<Map<String, B>>() { + }.getType()); + assertEquals("world", maps.get("hello").text); + assertEquals("name", maps.get("hello2").text); + assertEquals(C.a, maps.get("hello2").c); + assertEquals(2, maps.size()); + } +} diff --git a/src/test/java/com/stanfy/gsonxml/test/SubClassTest.java b/src/test/java/com/stanfy/gsonxml/test/SubClassTest.java new file mode 100644 index 0000000..fb903be --- /dev/null +++ b/src/test/java/com/stanfy/gsonxml/test/SubClassTest.java @@ -0,0 +1,40 @@ +package com.stanfy.gsonxml.test; + +import com.google.gson.annotations.SerializedName; +import com.stanfy.gsonxml.GsonXmlBuilder; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SubClassTest { + public static class A { + public A(){ + + } + @SerializedName("b") + B b; + + class B { + @SerializedName("@id") + long id; + @SerializedName("$") + String name; + } + } + + static final String XML = + " <a>" + + " <b id=\"1\">test</b>" + + "</a>"; + + @Test + public void test() { + final A a = new GsonXmlBuilder() + .setXmlParserCreator(SimpleXmlReaderTest.PARSER_CREATOR) + .create() + .fromXml(XML, A.class); + assertEquals(1, a.b.id); + assertEquals("test", a.b.name); + } +} From e779fdee5f82d2bf3c15658fa3248317e7100a71 Mon Sep 17 00:00:00 2001 From: keyongyu <247321453@qq.com> Date: Thu, 17 Mar 2016 09:04:53 +0800 Subject: [PATCH 3/3] test --- src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java | 3 +-- src/test/java/com/stanfy/gsonxml/issues/Issue7.java | 3 ++- src/test/java/com/stanfy/gsonxml/test/RssTest.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java b/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java index 0046f9a..f5ab622 100644 --- a/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java +++ b/src/main/java/com/stanfy/gsonxml/GsonXmlBuilder.java @@ -176,8 +176,7 @@ public GsonXmlBuilder setRootArrayPrimitive(final boolean rootArrayPrimitive) { } /** - * < to < - * + * @param decodeString charset * @return this instance for chaining */ public GsonXmlBuilder setDecodeString(final boolean decodeString) { diff --git a/src/test/java/com/stanfy/gsonxml/issues/Issue7.java b/src/test/java/com/stanfy/gsonxml/issues/Issue7.java index 571f28c..d6fcfe8 100644 --- a/src/test/java/com/stanfy/gsonxml/issues/Issue7.java +++ b/src/test/java/com/stanfy/gsonxml/issues/Issue7.java @@ -4,7 +4,7 @@ import com.stanfy.gsonxml.GsonXml; import com.stanfy.gsonxml.GsonXmlBuilder; import com.stanfy.gsonxml.XmlParserCreator; -import junit.framework.Assert; + import org.junit.Test; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; @@ -18,6 +18,7 @@ * Issue #7. * https://github.com/stanfy/gson-xml/issues/7 */ +@SuppressWarnings("deprecation") public class Issue7 { static class A { diff --git a/src/test/java/com/stanfy/gsonxml/test/RssTest.java b/src/test/java/com/stanfy/gsonxml/test/RssTest.java index 6257257..64aaa1d 100644 --- a/src/test/java/com/stanfy/gsonxml/test/RssTest.java +++ b/src/test/java/com/stanfy/gsonxml/test/RssTest.java @@ -26,7 +26,7 @@ public class RssTest extends AbstractXmlTest { + " <item>\n" + " <id>1</id>\n" + " <pubDate><![CDATA[Tue, 10 Jul 2012 10:43:36 +0300]]></pubDate>\n" - + " <title><![CDATA[Some text<"]]>\n" + + " <![CDATA[Some text]]>\n" + " \n" + " \n" + " " @@ -58,7 +58,7 @@ public static class Item { public void rssTest() throws Exception { final Rss feed = createGsonXml().fromXml(XML, Rss.class); assertEquals(1, feed.channel.items.get(0).id); - assertEquals("Some text<\"", feed.channel.items.get(0).title); + assertEquals("Some text", feed.channel.items.get(0).title); assertEquals("channel title<", feed.channel.title); assertEquals(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US).parse("Tue, 10 Jul 2012 10:43:36 +0300"), feed.channel.items.get(0).pubDate); }