From 3626e24c90e1029474fd21f47a02ec94361d330e Mon Sep 17 00:00:00 2001 From: REAndroid Date: Thu, 1 Dec 2022 14:37:12 -0500 Subject: [PATCH] V1.0.3 (to/from json convert) --- build.gradle | 1 - libs/json.jar | Bin 69370 -> 0 bytes .../java/com/reandroid/lib/apk/ApkModule.java | 41 + .../java/com/reandroid/lib/apk/ApkUtil.java | 1 + .../java/com/reandroid/lib/apk/ResFile.java | 30 + .../lib/arsc/array/EntryBlockArray.java | 8 +- .../lib/arsc/array/LibraryInfoArray.java | 9 +- .../lib/arsc/array/PackageArray.java | 9 +- .../lib/arsc/array/ResValueBagItemArray.java | 6 +- .../lib/arsc/array/ResXmlAttributeArray.java | 9 +- .../lib/arsc/array/ResXmlIDArray.java | 27 +- .../lib/arsc/array/SpecTypePairArray.java | 8 +- .../reandroid/lib/arsc/array/StringArray.java | 8 +- .../reandroid/lib/arsc/array/StyleArray.java | 6 +- .../lib/arsc/array/TableStringArray.java | 1 - .../lib/arsc/array/TypeBlockArray.java | 8 +- .../lib/arsc/chunk/PackageBlock.java | 7 +- .../reandroid/lib/arsc/chunk/SpecBlock.java | 6 +- .../reandroid/lib/arsc/chunk/TableBlock.java | 9 +- .../reandroid/lib/arsc/chunk/TypeBlock.java | 6 +- .../lib/arsc/chunk/xml/ResIdBuilder.java | 63 + .../lib/arsc/chunk/xml/ResXmlAttribute.java | 42 +- .../lib/arsc/chunk/xml/ResXmlBlock.java | 115 +- .../lib/arsc/chunk/xml/ResXmlElement.java | 102 +- .../lib/arsc/chunk/xml/ResXmlIDMap.java | 14 +- .../lib/arsc/container/SpecTypePair.java | 6 +- .../com/reandroid/lib/arsc/item/ResXmlID.java | 30 +- .../reandroid/lib/arsc/item/ResXmlString.java | 4 + .../reandroid/lib/arsc/item/StringItem.java | 6 +- .../reandroid/lib/arsc/item/StyleItem.java | 8 +- .../lib/arsc/model/StyleSpanInfo.java | 6 +- .../lib/arsc/pool/BaseStringPool.java | 23 +- .../lib/arsc/pool/TableStringPool.java | 2 - .../lib/arsc/value/BaseResValue.java | 6 +- .../reandroid/lib/arsc/value/EntryBlock.java | 7 +- .../reandroid/lib/arsc/value/LibraryInfo.java | 6 +- .../reandroid/lib/arsc/value/ResConfig.java | 6 +- .../reandroid/lib/arsc/value/ResValueBag.java | 3 +- .../lib/arsc/value/ResValueBagItem.java | 2 +- .../reandroid/lib/arsc/value/ResValueInt.java | 2 +- src/main/java/com/reandroid/lib/json/CDL.java | 165 ++ .../java/com/reandroid/lib/json/Cookie.java | 138 ++ .../com/reandroid/lib/json/CookieList.java | 35 + .../java/com/reandroid/lib/json/HTTP.java | 81 + .../com/reandroid/lib/json/HTTPTokener.java | 36 + .../com/reandroid/lib/json/JSONArray.java | 755 +++++++++ .../json/{JsonItem.java => JSONConvert.java} | 2 +- .../com/reandroid/lib/json/JSONException.java | 19 + .../java/com/reandroid/lib/json/JSONItem.java | 49 + .../java/com/reandroid/lib/json/JSONML.java | 416 +++++ .../com/reandroid/lib/json/JSONObject.java | 1405 +++++++++++++++++ .../com/reandroid/lib/json/JSONPointer.java | 168 ++ .../lib/json/JSONPointerException.java | 14 + .../lib/json/JSONPropertyIgnore.java | 14 + .../reandroid/lib/json/JSONPropertyName.java | 17 + .../com/reandroid/lib/json/JSONString.java | 6 + .../com/reandroid/lib/json/JSONStringer.java | 15 + .../com/reandroid/lib/json/JSONTokener.java | 339 ++++ .../com/reandroid/lib/json/JSONWriter.java | 219 +++ .../java/com/reandroid/lib/json/JsonUtil.java | 68 +- .../java/com/reandroid/lib/json/Property.java | 35 + src/main/java/com/reandroid/lib/json/XML.java | 600 +++++++ .../lib/json/XMLParserConfiguration.java | 120 ++ .../com/reandroid/lib/json/XMLTokener.java | 312 ++++ .../lib/json/XMLXsiTypeConverter.java | 5 + 65 files changed, 5446 insertions(+), 240 deletions(-) delete mode 100755 libs/json.jar create mode 100644 src/main/java/com/reandroid/lib/arsc/chunk/xml/ResIdBuilder.java create mode 100644 src/main/java/com/reandroid/lib/json/CDL.java create mode 100644 src/main/java/com/reandroid/lib/json/Cookie.java create mode 100644 src/main/java/com/reandroid/lib/json/CookieList.java create mode 100644 src/main/java/com/reandroid/lib/json/HTTP.java create mode 100644 src/main/java/com/reandroid/lib/json/HTTPTokener.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONArray.java rename src/main/java/com/reandroid/lib/json/{JsonItem.java => JSONConvert.java} (64%) create mode 100644 src/main/java/com/reandroid/lib/json/JSONException.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONItem.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONML.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONObject.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONPointer.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONPointerException.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONPropertyIgnore.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONPropertyName.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONString.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONStringer.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONTokener.java create mode 100644 src/main/java/com/reandroid/lib/json/JSONWriter.java create mode 100644 src/main/java/com/reandroid/lib/json/Property.java create mode 100644 src/main/java/com/reandroid/lib/json/XML.java create mode 100644 src/main/java/com/reandroid/lib/json/XMLParserConfiguration.java create mode 100644 src/main/java/com/reandroid/lib/json/XMLTokener.java create mode 100644 src/main/java/com/reandroid/lib/json/XMLXsiTypeConverter.java diff --git a/build.gradle b/build.gradle index 817d1c8..1bac861 100755 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ repositories { dependencies { compile(files("$rootProject.projectDir/libs/ArchiveUtil.jar")) - compile(files("$rootProject.projectDir/libs/json.jar")) } processResources { diff --git a/libs/json.jar b/libs/json.jar deleted file mode 100755 index a9eab7ade733cb9e67c2cc35b915c4820bd7e4a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69370 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 diff --git a/src/main/java/com/reandroid/lib/apk/ApkModule.java b/src/main/java/com/reandroid/lib/apk/ApkModule.java index 29aecbc..20a6725 100644 --- a/src/main/java/com/reandroid/lib/apk/ApkModule.java +++ b/src/main/java/com/reandroid/lib/apk/ApkModule.java @@ -12,6 +12,7 @@ import com.reandroid.lib.arsc.pool.TableStringPool; import com.reandroid.lib.arsc.value.EntryBlock; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.*; @@ -163,6 +164,46 @@ public class ApkModule { public void setLoadDefaultFramework(boolean loadDefaultFramework) { this.loadDefaultFramework = loadDefaultFramework; } + public void convertToJson(File outDir) throws IOException { + Set convertedFiles=new HashSet<>(); + if(hasAndroidManifestBlock()){ + AndroidManifestBlock manifestBlock=getAndroidManifestBlock(); + String fileName=AndroidManifestBlock.FILE_NAME+ApkUtil.JSON_FILE_EXTENSION; + File file=new File(outDir, fileName); + manifestBlock.toJson().write(file); + convertedFiles.add(AndroidManifestBlock.FILE_NAME); + } + if(hasTableBlock()){ + TableBlock tableBlock=getTableBlock(); + String fileName=TableBlock.FILE_NAME+ApkUtil.JSON_FILE_EXTENSION; + File file=new File(outDir, fileName); + tableBlock.toJson().write(file); + convertedFiles.add(TableBlock.FILE_NAME); + } + List resFileList=listResFiles(); + for(ResFile resFile:resFileList){ + boolean convertOk=resFile.dumpToJson(outDir); + if(convertOk){ + convertedFiles.add(resFile.getFilePath()); + } + } + List allSources = getApkArchive().listInputSources(); + for(InputSource inputSource:allSources){ + String path=inputSource.getAlias(); + if(convertedFiles.contains(path)){ + continue; + } + path=path.replace('/', File.separatorChar); + File file=new File(outDir, path); + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + inputSource.write(outputStream); + outputStream.close(); + } + } public static ApkModule loadApkFile(File apkFile) throws IOException { APKArchive archive=APKArchive.loadZippedApk(apkFile); return new ApkModule(archive); diff --git a/src/main/java/com/reandroid/lib/apk/ApkUtil.java b/src/main/java/com/reandroid/lib/apk/ApkUtil.java index adc5097..90605de 100644 --- a/src/main/java/com/reandroid/lib/apk/ApkUtil.java +++ b/src/main/java/com/reandroid/lib/apk/ApkUtil.java @@ -12,4 +12,5 @@ public class ApkUtil { } return path; } + public static final String JSON_FILE_EXTENSION=".a.json"; } diff --git a/src/main/java/com/reandroid/lib/apk/ResFile.java b/src/main/java/com/reandroid/lib/apk/ResFile.java index 2c856ec..111be6a 100644 --- a/src/main/java/com/reandroid/lib/apk/ResFile.java +++ b/src/main/java/com/reandroid/lib/apk/ResFile.java @@ -1,13 +1,19 @@ package com.reandroid.lib.apk; import com.reandroid.archive.InputSource; +import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock; import com.reandroid.lib.arsc.value.EntryBlock; +import com.reandroid.lib.json.JSONObject; +import java.io.File; +import java.io.IOException; import java.util.List; public class ResFile { private final List entryBlockList; private final InputSource inputSource; + private boolean mBinXml; + private boolean mBinXmlChecked; public ResFile(InputSource inputSource, List entryBlockList){ this.inputSource=inputSource; this.entryBlockList=entryBlockList; @@ -24,6 +30,30 @@ public class ResFile { public InputSource getInputSource() { return inputSource; } + public boolean isBinaryXml(){ + if(mBinXmlChecked){ + return mBinXml; + } + mBinXmlChecked=true; + try { + mBinXml=ResXmlBlock.isResXmlBlock(getInputSource().openStream()); + } catch (IOException exception) { + } + return mBinXml; + } + public boolean dumpToJson(File rootDir) throws IOException { + if(!isBinaryXml()){ + return false; + } + String fileName=getFilePath()+ApkUtil.JSON_FILE_EXTENSION; + fileName=fileName.replace('/', File.separatorChar); + File file=new File(rootDir, fileName); + ResXmlBlock resXmlBlock=new ResXmlBlock(); + resXmlBlock.readBytes(getInputSource().openStream()); + JSONObject jsonObject=resXmlBlock.toJson(); + jsonObject.write(file); + return true; + } @Override public String toString(){ return getFilePath(); diff --git a/src/main/java/com/reandroid/lib/arsc/array/EntryBlockArray.java b/src/main/java/com/reandroid/lib/arsc/array/EntryBlockArray.java index 3182eef..f2174e0 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/EntryBlockArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/EntryBlockArray.java @@ -3,12 +3,12 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.value.EntryBlock; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; -public class EntryBlockArray extends OffsetBlockArray implements JsonItem { +public class EntryBlockArray extends OffsetBlockArray implements JSONConvert { public EntryBlockArray(IntegerArray offsets, IntegerItem itemCount, IntegerItem itemStart){ super(offsets, itemCount, itemStart); } diff --git a/src/main/java/com/reandroid/lib/arsc/array/LibraryInfoArray.java b/src/main/java/com/reandroid/lib/arsc/array/LibraryInfoArray.java index 8e74b06..7e1efd9 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/LibraryInfoArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/LibraryInfoArray.java @@ -1,17 +1,16 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.base.BlockArray; -import com.reandroid.lib.arsc.container.SpecTypePair; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.value.LibraryInfo; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; -public class LibraryInfoArray extends BlockArray implements JsonItem { +public class LibraryInfoArray extends BlockArray implements JSONConvert { private final IntegerItem mInfoCount; public LibraryInfoArray(IntegerItem infoCount){ this.mInfoCount=infoCount; diff --git a/src/main/java/com/reandroid/lib/arsc/array/PackageArray.java b/src/main/java/com/reandroid/lib/arsc/array/PackageArray.java index 42b1942..df2696b 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/PackageArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/PackageArray.java @@ -3,18 +3,17 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.base.BlockArray; import com.reandroid.lib.arsc.chunk.PackageBlock; -import com.reandroid.lib.arsc.container.SpecTypePair; import com.reandroid.lib.arsc.io.BlockLoad; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.Iterator; -public class PackageArray extends BlockArray implements BlockLoad, JsonItem { +public class PackageArray extends BlockArray implements BlockLoad, JSONConvert { private final IntegerItem mPackageCount; public PackageArray(IntegerItem packageCount){ this.mPackageCount=packageCount; diff --git a/src/main/java/com/reandroid/lib/arsc/array/ResValueBagItemArray.java b/src/main/java/com/reandroid/lib/arsc/array/ResValueBagItemArray.java index a967259..63d9fc7 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/ResValueBagItemArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/ResValueBagItemArray.java @@ -2,10 +2,10 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.base.BlockArray; import com.reandroid.lib.arsc.value.ResValueBagItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; -public class ResValueBagItemArray extends BlockArray implements JsonItem { +public class ResValueBagItemArray extends BlockArray implements JSONConvert { public ResValueBagItemArray(){ super(); } diff --git a/src/main/java/com/reandroid/lib/arsc/array/ResXmlAttributeArray.java b/src/main/java/com/reandroid/lib/arsc/array/ResXmlAttributeArray.java index ea30efb..9da8ed7 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/ResXmlAttributeArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/ResXmlAttributeArray.java @@ -2,14 +2,13 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.base.BlockArray; -import com.reandroid.lib.arsc.chunk.xml.ResXmlStartElement; import com.reandroid.lib.arsc.header.HeaderBlock; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.chunk.xml.ResXmlAttribute; import com.reandroid.lib.arsc.item.ShortItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.ArrayList; @@ -18,7 +17,7 @@ import java.util.Comparator; import java.util.List; public class ResXmlAttributeArray extends BlockArray - implements Comparator, JsonItem { + implements Comparator, JSONConvert { private final HeaderBlock mHeaderBlock; private final ShortItem mAttributeStart; private final ShortItem mAttributeCount; diff --git a/src/main/java/com/reandroid/lib/arsc/array/ResXmlIDArray.java b/src/main/java/com/reandroid/lib/arsc/array/ResXmlIDArray.java index a88758f..e3d6a37 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/ResXmlIDArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/ResXmlIDArray.java @@ -4,15 +4,15 @@ import com.reandroid.lib.arsc.base.BlockArray; import com.reandroid.lib.arsc.header.HeaderBlock; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.ResXmlID; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class ResXmlIDArray extends BlockArray implements JsonItem { +public class ResXmlIDArray extends BlockArray { private final HeaderBlock mHeaderBlock; private final Map mResIdMap; private boolean mUpdated; @@ -89,23 +89,4 @@ public class ResXmlIDArray extends BlockArray implements JsonItem implements JsonItem { +public class SpecTypePairArray extends BlockArray implements JSONConvert { public SpecTypePairArray(){ super(); } diff --git a/src/main/java/com/reandroid/lib/arsc/array/StringArray.java b/src/main/java/com/reandroid/lib/arsc/array/StringArray.java index fa23bda..6efefeb 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/StringArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/StringArray.java @@ -3,14 +3,14 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.StringItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.util.ArrayList; import java.util.List; -public abstract class StringArray extends OffsetBlockArray implements JsonItem { +public abstract class StringArray extends OffsetBlockArray implements JSONConvert { private boolean mUtf8; public StringArray(IntegerArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { diff --git a/src/main/java/com/reandroid/lib/arsc/array/StyleArray.java b/src/main/java/com/reandroid/lib/arsc/array/StyleArray.java index 494dc6c..1051164 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/StyleArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/StyleArray.java @@ -5,12 +5,12 @@ import com.reandroid.lib.arsc.item.ByteArray; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.StyleItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; import java.io.IOException; -public class StyleArray extends OffsetBlockArray implements JsonItem { +public class StyleArray extends OffsetBlockArray implements JSONConvert { public StyleArray(IntegerArray offsets, IntegerItem itemCount, IntegerItem itemStart) { super(offsets, itemCount, itemStart); setEndBytes(END_BYTE); diff --git a/src/main/java/com/reandroid/lib/arsc/array/TableStringArray.java b/src/main/java/com/reandroid/lib/arsc/array/TableStringArray.java index fe13ce3..086e0c1 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/TableStringArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/TableStringArray.java @@ -3,7 +3,6 @@ package com.reandroid.lib.arsc.array; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.TableString; -import org.json.JSONArray; public class TableStringArray extends StringArray { public TableStringArray(IntegerArray offsets, IntegerItem itemCount, IntegerItem itemStart, boolean is_utf8) { diff --git a/src/main/java/com/reandroid/lib/arsc/array/TypeBlockArray.java b/src/main/java/com/reandroid/lib/arsc/array/TypeBlockArray.java index 238f1b8..55c8ff5 100755 --- a/src/main/java/com/reandroid/lib/arsc/array/TypeBlockArray.java +++ b/src/main/java/com/reandroid/lib/arsc/array/TypeBlockArray.java @@ -10,16 +10,16 @@ import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.TypeString; import com.reandroid.lib.arsc.value.EntryBlock; import com.reandroid.lib.arsc.value.ResConfig; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.AbstractList; import java.util.ArrayList; import java.util.List; -public class TypeBlockArray extends BlockArray implements JsonItem { +public class TypeBlockArray extends BlockArray implements JSONConvert { private byte mTypeId; public TypeBlockArray(){ super(); diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java index 1b79380..d8fa9e4 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/PackageBlock.java @@ -9,19 +9,18 @@ import com.reandroid.lib.arsc.group.EntryGroup; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.PackageName; import com.reandroid.lib.arsc.item.ReferenceItem; -import com.reandroid.lib.arsc.item.TypeString; import com.reandroid.lib.arsc.pool.SpecStringPool; import com.reandroid.lib.arsc.pool.TableStringPool; import com.reandroid.lib.arsc.pool.TypeStringPool; import com.reandroid.lib.arsc.value.EntryBlock; import com.reandroid.lib.arsc.value.LibraryInfo; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.util.*; -public class PackageBlock extends BaseChunk implements JsonItem { +public class PackageBlock extends BaseChunk implements JSONConvert { private final IntegerItem mPackageId; private final PackageName mPackageName; diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/SpecBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/SpecBlock.java index 5bd4051..1c0098c 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/SpecBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/SpecBlock.java @@ -7,12 +7,12 @@ import com.reandroid.lib.arsc.io.BlockLoad; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; -public class SpecBlock extends BaseTypeBlock implements BlockLoad , JsonItem { +public class SpecBlock extends BaseTypeBlock implements BlockLoad , JSONConvert { private final IntegerArray mOffsets; public SpecBlock() { super(ChunkType.SPEC, 1); diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java index 25e5935..542fa3e 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/TableBlock.java @@ -1,23 +1,22 @@ package com.reandroid.lib.arsc.chunk; import com.reandroid.lib.arsc.array.PackageArray; -import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock; import com.reandroid.lib.arsc.group.EntryGroup; import com.reandroid.lib.arsc.header.HeaderBlock; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.pool.TableStringPool; import com.reandroid.lib.common.Frameworks; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.*; import java.util.Collection; import java.util.HashSet; import java.util.Set; -public class TableBlock extends BaseChunk implements JsonItem { +public class TableBlock extends BaseChunk implements JSONConvert { private final IntegerItem mPackageCount; private final TableStringPool mTableStringPool; private final PackageArray mPackageArray; diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/TypeBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/TypeBlock.java index daa321f..5956a30 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/TypeBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/TypeBlock.java @@ -8,14 +8,14 @@ import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.TypeString; import com.reandroid.lib.arsc.value.EntryBlock; import com.reandroid.lib.arsc.value.ResConfig; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -public class TypeBlock extends BaseTypeBlock implements JsonItem { +public class TypeBlock extends BaseTypeBlock implements JSONConvert { private final IntegerItem mEntriesStart; private final ResConfig mResConfig; private final IntegerArray mEntryOffsets; diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResIdBuilder.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResIdBuilder.java new file mode 100644 index 0000000..42e0dc8 --- /dev/null +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResIdBuilder.java @@ -0,0 +1,63 @@ +package com.reandroid.lib.arsc.chunk.xml; + +import com.reandroid.lib.arsc.array.ResXmlIDArray; +import com.reandroid.lib.arsc.array.StringArray; +import com.reandroid.lib.arsc.item.ResXmlID; +import com.reandroid.lib.arsc.item.ResXmlString; +import com.reandroid.lib.arsc.pool.ResXmlStringPool; + +import java.util.*; + +public class ResIdBuilder implements Comparator { + private final Map mIdNameMap; + public ResIdBuilder(){ + this.mIdNameMap=new HashMap<>(); + } + public void buildTo(ResXmlIDMap resXmlIDMap){ + ResXmlStringPool stringPool = resXmlIDMap.getXmlStringPool(); + StringArray xmlStringsArray = stringPool.getStringsArray(); + ResXmlIDArray xmlIDArray = resXmlIDMap.getResXmlIDArray(); + List idList=getSortedIds(); + int size = idList.size(); + xmlStringsArray.ensureSize(size); + xmlIDArray.ensureSize(size); + for(int i=0;i0){ + ResXmlString replaceXmlString=new ResXmlString(xmlString.isUtf8(), xmlString.get()); + xmlStringsArray.setItem(i, replaceXmlString); + xmlStringsArray.add(xmlString); + xmlString=replaceXmlString; + } + ResXmlID xmlID = xmlIDArray.get(i); + if(xmlID.getReferencedList().size()>0){ + ResXmlID replaceXmlId = new ResXmlID(xmlID.get()); + xmlIDArray.setItem(i, replaceXmlId); + xmlIDArray.add(xmlID); + xmlID=replaceXmlId; + } + int resourceId = idList.get(i); + String name = mIdNameMap.get(resourceId); + xmlID.set(resourceId); + xmlString.set(name); + } + } + public void add(int id, String name){ + if(id==0){ + return; + } + if(name==null){ + name=""; + } + mIdNameMap.put(id, name); + } + private List getSortedIds(){ + List results=new ArrayList<>(mIdNameMap.keySet()); + results.sort(this); + return results; + } + @Override + public int compare(Integer i1, Integer i2) { + return i1.compareTo(i2); + } +} diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlAttribute.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlAttribute.java index b0d906b..6d05059 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlAttribute.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlAttribute.java @@ -3,14 +3,15 @@ package com.reandroid.lib.arsc.chunk.xml; import com.reandroid.lib.arsc.array.ResXmlIDArray; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.container.FixedBlockContainer; +import com.reandroid.lib.arsc.decoder.ValueDecoder; import com.reandroid.lib.arsc.item.*; import com.reandroid.lib.arsc.pool.ResXmlStringPool; import com.reandroid.lib.arsc.value.ValueType; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; public class ResXmlAttribute extends FixedBlockContainer - implements Comparable, JsonItem { + implements Comparable, JSONConvert { private final IntegerItem mNamespaceReference; private final IntegerItem mNameReference; private final IntegerItem mValueStringReference; @@ -364,7 +365,6 @@ public class ResXmlAttribute extends FixedBlockContainer jsonObject.put(NAME_name, getName()); jsonObject.put(NAME_id, getNameResourceID()); jsonObject.put(NAME_namespace_uri, getNamespace()); - jsonObject.put(NAME_namespace_prefix, getNamePrefix()); ValueType valueType=getValueType(); jsonObject.put(NAME_value_type, valueType.name()); if(valueType==ValueType.STRING){ @@ -379,12 +379,13 @@ public class ResXmlAttribute extends FixedBlockContainer @Override public void fromJson(JSONObject json) { String name = json.getString(NAME_name); - int id = json.optInt(NAME_id); + int id = json.optInt(NAME_id, 0); setName(name, id); - String uri= json.optString(NAME_namespace_uri); - String prefix= json.optString(NAME_namespace_prefix); - ResXmlStartNamespace ns = getParentResXmlElement().getOrCreateNamespace(uri, prefix); - setNamespaceReference(ns.getUriReference()); + String uri= json.optString(NAME_namespace_uri, null); + if(uri!=null){ + ResXmlStartNamespace ns = getParentResXmlElement().getStartNamespaceByUri(uri); + setNamespaceReference(ns.getUriReference()); + } ValueType valueType=ValueType.fromName(json.getString(NAME_value_type)); if(valueType==ValueType.STRING){ setValueAsString(json.getString(NAME_data)); @@ -403,11 +404,19 @@ public class ResXmlAttribute extends FixedBlockContainer if(id>0){ fullName=fullName+"(@"+String.format("0x%08x",id)+")"; } - String valStr=getValueString(); + String valStr; + ValueType valueType=getValueType(); + if(valueType==ValueType.STRING){ + valStr=getValueAsString(); + }else if (valueType==ValueType.INT_BOOLEAN){ + valStr=String.valueOf(getValueAsBoolean()); + }else { + valStr="["+valueType+"] "+getRawValue(); + } if(valStr!=null){ return fullName+"=\""+valStr+"\""; } - return fullName+"["+getValueType()+"]=\""+getRawValue()+"\""; + return fullName+"["+valueType+"]=\""+getRawValue()+"\""; } StringBuilder builder=new StringBuilder(); builder.append(getClass().getSimpleName()); @@ -423,10 +432,9 @@ public class ResXmlAttribute extends FixedBlockContainer builder.append("}"); return builder.toString(); } - private static final String NAME_id="id"; - private static final String NAME_value_type="value_type"; - private static final String NAME_name="name"; - private static final String NAME_namespace_uri ="namespace_uri"; - private static final String NAME_namespace_prefix ="namespace_name"; - private static final String NAME_data="data"; + static final String NAME_id="id"; + static final String NAME_value_type="value_type"; + static final String NAME_name="name"; + static final String NAME_namespace_uri ="namespace_uri"; + static final String NAME_data="data"; } diff --git a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java index e4f9cd5..5a18555 100755 --- a/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/chunk/xml/ResXmlBlock.java @@ -6,13 +6,18 @@ import com.reandroid.lib.arsc.container.SingleBlockContainer; import com.reandroid.lib.arsc.header.HeaderBlock; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.pool.ResXmlStringPool; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.arsc.value.ValueType; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; -public class ResXmlBlock extends BaseChunk implements JsonItem { +public class ResXmlBlock extends BaseChunk implements JSONConvert { private final ResXmlStringPool mResXmlStringPool; private final ResXmlIDMap mResXmlIDMap; private ResXmlElement mResXmlElement; @@ -135,22 +140,106 @@ public class ResXmlBlock extends BaseChunk implements JsonItem { @Override public JSONObject toJson() { JSONObject jsonObject=new JSONObject(); - jsonObject.put(NAME_element, getResXmlElement().toJson()); - JSONArray pool =getStringPool().toJson(); + jsonObject.put(ResXmlBlock.NAME_element, getResXmlElement().toJson()); + JSONArray pool = getStringPool().toJson(); if(pool!=null){ - jsonObject.put(NAME_styled_strings, getResXmlElement().toJson()); - } - JSONArray idArray = getResXmlIDMap().getResXmlIDArray().toJson(); - if(idArray!=null){ - jsonObject.put("resource_ids", idArray); + jsonObject.put(ResXmlBlock.NAME_styled_strings, getResXmlElement().toJson()); } return jsonObject; } @Override public void fromJson(JSONObject json) { - // TODO - throw new IllegalArgumentException("Not implemented yet"); + onFromJson(json); + ResXmlElement xmlElement=getResXmlElement(); + xmlElement.fromJson(json.optJSONObject(ResXmlBlock.NAME_element)); + refresh(); + } + private void onFromJson(JSONObject json){ + List attributeList=recursiveAttributes(json.optJSONObject(ResXmlBlock.NAME_element)); + buildResourceIds(attributeList); + Set allStrings=recursiveStrings(json.optJSONObject(ResXmlBlock.NAME_element)); + ResXmlStringPool stringPool = getStringPool(); + stringPool.addAllStrings(allStrings); + stringPool.refresh(); + } + private void buildResourceIds(List attributeList){ + ResIdBuilder builder=new ResIdBuilder(); + for(JSONObject attribute:attributeList){ + int id=attribute.getInt(ResXmlAttribute.NAME_id); + if(id==0){ + continue; + } + String name=attribute.getString(ResXmlAttribute.NAME_name); + builder.add(id, name); + } + builder.buildTo(getResXmlIDMap()); + } + private List recursiveAttributes(JSONObject elementJson){ + List results = new ArrayList<>(); + if(elementJson==null){ + return results; + } + JSONArray attributes = elementJson.optJSONArray(ResXmlElement.NAME_attributes); + if(attributes != null){ + int length = attributes.length(); + for(int i=0; i recursiveStrings(JSONObject elementJson){ + Set results = new HashSet<>(); + if(elementJson==null){ + return results; + } + results.add(elementJson.optString(ResXmlElement.NAME_namespace_uri)); + results.add(elementJson.optString(ResXmlElement.NAME_name)); + JSONArray namespaces=elementJson.optJSONArray(ResXmlElement.NAME_namespaces); + if(namespaces != null){ + int length = namespaces.length(); + for(int i=0; i { +public class ResXmlElement extends FixedBlockContainer implements JSONConvert { private final BlockList mStartNamespaceList; private final SingleBlockContainer mStartElementContainer; private final BlockList mBody; @@ -50,23 +49,19 @@ public class ResXmlElement extends FixedBlockContainer implements JsonItem implements JsonItem { +public class SpecTypePair extends BlockContainer implements JSONConvert { private final Block[] mChildes; private final SpecBlock mSpecBlock; private final TypeBlockArray mTypeBlockArray; diff --git a/src/main/java/com/reandroid/lib/arsc/item/ResXmlID.java b/src/main/java/com/reandroid/lib/arsc/item/ResXmlID.java index 06a4fd4..67841c6 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/ResXmlID.java +++ b/src/main/java/com/reandroid/lib/arsc/item/ResXmlID.java @@ -3,14 +3,14 @@ package com.reandroid.lib.arsc.item; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.chunk.xml.ResXmlBlock; import com.reandroid.lib.arsc.pool.ResXmlStringPool; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.List; -public class ResXmlID extends IntegerItem implements JsonItem { +public class ResXmlID extends IntegerItem { private final List mReferencedList; public ResXmlID(int resId){ super(resId); @@ -53,7 +53,17 @@ public class ResXmlID extends IntegerItem implements JsonItem { public void onIndexChanged(int oldIndex, int newIndex){ reUpdateReferences(newIndex); } - + public String getName(){ + ResXmlStringPool stringPool=getXmlStringPool(); + if(stringPool==null){ + return null; + } + ResXmlString xmlString = stringPool.get(getIndex()); + if(xmlString==null){ + return null; + } + return xmlString.getHtml(); + } private ResXmlStringPool getXmlStringPool(){ Block parent=this; while (parent!=null){ @@ -65,18 +75,6 @@ public class ResXmlID extends IntegerItem implements JsonItem { return null; } @Override - public JSONObject toJson() { - JSONObject jsonObject=new JSONObject(); - jsonObject.put("id", get()); - jsonObject.put("name", getXmlStringPool().get(getIndex()).getHtml()); - return jsonObject; - } - @Override - public void fromJson(JSONObject json) { - //TODO - throw new IllegalArgumentException("Not implemented yet"); - } - @Override public String toString(){ return getIndex()+": "+String.format("0x%08x", get()); } diff --git a/src/main/java/com/reandroid/lib/arsc/item/ResXmlString.java b/src/main/java/com/reandroid/lib/arsc/item/ResXmlString.java index 219cf6e..e4ff94e 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/ResXmlString.java +++ b/src/main/java/com/reandroid/lib/arsc/item/ResXmlString.java @@ -4,4 +4,8 @@ public class ResXmlString extends StringItem { public ResXmlString(boolean utf8) { super(utf8); } + public ResXmlString(boolean utf8, String value) { + this(utf8); + set(value); + } } diff --git a/src/main/java/com/reandroid/lib/arsc/item/StringItem.java b/src/main/java/com/reandroid/lib/arsc/item/StringItem.java index a4d41d0..25ee7d9 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/StringItem.java +++ b/src/main/java/com/reandroid/lib/arsc/item/StringItem.java @@ -3,8 +3,8 @@ package com.reandroid.lib.arsc.item; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.pool.BaseStringPool; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; @@ -17,7 +17,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -public class StringItem extends BlockItem implements JsonItem { +public class StringItem extends BlockItem implements JSONConvert { private String mCache; private boolean mUtf8; private final List mReferencedList; diff --git a/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java b/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java index 509ad0f..9ae950c 100755 --- a/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java +++ b/src/main/java/com/reandroid/lib/arsc/item/StyleItem.java @@ -4,16 +4,16 @@ import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.model.StyleSpanInfo; import com.reandroid.lib.arsc.pool.BaseStringPool; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.AbstractList; import java.util.ArrayList; import java.util.List; -public class StyleItem extends IntegerArray implements JsonItem { +public class StyleItem extends IntegerArray implements JSONConvert { private List mSpanInfoList; public StyleItem() { super(); diff --git a/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java b/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java index 4cb7a25..72d0e87 100755 --- a/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java +++ b/src/main/java/com/reandroid/lib/arsc/model/StyleSpanInfo.java @@ -1,9 +1,9 @@ package com.reandroid.lib.arsc.model; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; -public class StyleSpanInfo implements JsonItem { +public class StyleSpanInfo implements JSONConvert { private String mTag; private int mFirst; private int mLast; diff --git a/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java b/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java index 69f2fa4..b750bc7 100755 --- a/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java +++ b/src/main/java/com/reandroid/lib/arsc/pool/BaseStringPool.java @@ -10,14 +10,14 @@ import com.reandroid.lib.arsc.io.BlockLoad; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.*; import com.reandroid.lib.arsc.pool.builder.StyleBuilder; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONArray; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONArray; import java.io.IOException; import java.util.*; -public abstract class BaseStringPool extends BaseChunk implements BlockLoad, JsonItem { +public abstract class BaseStringPool extends BaseChunk implements BlockLoad, JSONConvert, Comparator { private final IntegerItem mCountStrings; private final IntegerItem mCountStyles; private final ShortItem mFlagUtf8; @@ -66,6 +66,19 @@ public abstract class BaseStringPool extends BaseChunk imp mUniqueMap=new HashMap<>(); } + public void addAllStrings(Collection stringList){ + List sortedList=new ArrayList<>(stringList); + sortedList.sort(this); + addAllStrings(sortedList); + } + public void addAllStrings(List sortedStringList){ + // call refreshUniqueIdMap() just in case elements modified somewhere + refreshUniqueIdMap(); + + for(String str:sortedStringList){ + getOrCreate(str); + } + } // call this after modifying string values public void refreshUniqueIdMap(){ reUpdateUniqueMap(); @@ -265,6 +278,10 @@ public abstract class BaseStringPool extends BaseChunk imp refreshUniqueIdMap(); refresh(); } + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + } private static final short UTF8_FLAG_VALUE=0x0100; private static final short FLAG_UTF8 = 0x0100; diff --git a/src/main/java/com/reandroid/lib/arsc/pool/TableStringPool.java b/src/main/java/com/reandroid/lib/arsc/pool/TableStringPool.java index 1a9bba0..3c6ad36 100755 --- a/src/main/java/com/reandroid/lib/arsc/pool/TableStringPool.java +++ b/src/main/java/com/reandroid/lib/arsc/pool/TableStringPool.java @@ -4,9 +4,7 @@ import com.reandroid.lib.arsc.array.StringArray; import com.reandroid.lib.arsc.array.TableStringArray; import com.reandroid.lib.arsc.item.IntegerArray; import com.reandroid.lib.arsc.item.IntegerItem; -import com.reandroid.lib.arsc.item.ReferenceItem; import com.reandroid.lib.arsc.item.TableString; -import org.json.JSONArray; public class TableStringPool extends BaseStringPool { public TableStringPool(boolean is_utf8) { diff --git a/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java b/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java index 10532b8..c8e853a 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java +++ b/src/main/java/com/reandroid/lib/arsc/value/BaseResValue.java @@ -3,10 +3,10 @@ package com.reandroid.lib.arsc.value; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.item.BlockItem; import com.reandroid.lib.arsc.item.ReferenceItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; -public abstract class BaseResValue extends BlockItem implements JsonItem { +public abstract class BaseResValue extends BlockItem implements JSONConvert { BaseResValue(int bytesLength){ super(bytesLength); } diff --git a/src/main/java/com/reandroid/lib/arsc/value/EntryBlock.java b/src/main/java/com/reandroid/lib/arsc/value/EntryBlock.java index 314568b..223d1e5 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/EntryBlock.java +++ b/src/main/java/com/reandroid/lib/arsc/value/EntryBlock.java @@ -10,8 +10,8 @@ import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.*; import com.reandroid.lib.arsc.pool.SpecStringPool; import com.reandroid.lib.arsc.pool.TableStringPool; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; @@ -19,7 +19,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; -public class EntryBlock extends Block implements JsonItem { +public class EntryBlock extends Block implements JSONConvert { private ShortItem mHeaderSize; private ShortItem mFlags; private IntegerItem mSpecReference; @@ -630,7 +630,6 @@ public class EntryBlock extends Block implements JsonItem { jsonObject.put(NAME_value, getResValue().toJson()); return jsonObject; } - @Override public void fromJson(JSONObject json) { if(json==null){ diff --git a/src/main/java/com/reandroid/lib/arsc/value/LibraryInfo.java b/src/main/java/com/reandroid/lib/arsc/value/LibraryInfo.java index 2d1bdc5..487e318 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/LibraryInfo.java +++ b/src/main/java/com/reandroid/lib/arsc/value/LibraryInfo.java @@ -5,13 +5,13 @@ import com.reandroid.lib.arsc.base.BlockCounter; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.PackageName; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.io.OutputStream; -public class LibraryInfo extends Block implements JsonItem { +public class LibraryInfo extends Block implements JSONConvert { private final IntegerItem mPackageId; private final PackageName mPackageName; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java b/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java index a287407..4feceb3 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResConfig.java @@ -6,13 +6,13 @@ import com.reandroid.lib.arsc.io.BlockLoad; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.ByteArray; import com.reandroid.lib.arsc.item.IntegerItem; -import com.reandroid.lib.json.JsonItem; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONConvert; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.util.Arrays; -public class ResConfig extends FixedBlockContainer implements BlockLoad, JsonItem { +public class ResConfig extends FixedBlockContainer implements BlockLoad, JSONConvert { private final IntegerItem configSize; private final ByteArray mValuesContainer; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java index 5823666..adf2d33 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueBag.java @@ -6,8 +6,7 @@ import com.reandroid.lib.arsc.base.BlockCounter; import com.reandroid.lib.arsc.io.BlockReader; import com.reandroid.lib.arsc.item.IntegerItem; import com.reandroid.lib.arsc.item.ReferenceItem; -import org.json.JSONArray; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONObject; import java.io.IOException; import java.io.OutputStream; diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java index f454a61..912068a 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueBagItem.java @@ -3,7 +3,7 @@ package com.reandroid.lib.arsc.value; import com.reandroid.lib.arsc.base.Block; import com.reandroid.lib.arsc.item.ReferenceItem; import com.reandroid.lib.arsc.item.TableString; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONObject; public class ResValueBagItem extends BaseResValueItem{ diff --git a/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java b/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java index 8b6c85a..84af73e 100755 --- a/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java +++ b/src/main/java/com/reandroid/lib/arsc/value/ResValueInt.java @@ -2,7 +2,7 @@ package com.reandroid.lib.arsc.value; import com.reandroid.lib.arsc.decoder.ValueDecoder; import com.reandroid.lib.arsc.item.TableString; -import org.json.JSONObject; +import com.reandroid.lib.json.JSONObject; public class ResValueInt extends BaseResValueItem { public ResValueInt() { diff --git a/src/main/java/com/reandroid/lib/json/CDL.java b/src/main/java/com/reandroid/lib/json/CDL.java new file mode 100644 index 0000000..c23c822 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/CDL.java @@ -0,0 +1,165 @@ +package com.reandroid.lib.json; + +public class CDL { + + private static String getValue(JSONTokener x) throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = x.next(); + } while (c == ' ' || c == '\t'); + switch (c) { + case 0: + return null; + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = x.next(); + if (c == q) { + //Handle escaped double-quote + char nextC = x.next(); + if(nextC != '\"') { + // if our quote was the end of the file, don't step + if(nextC > 0) { + x.back(); + } + break; + } + } + if (c == 0 || c == '\n' || c == '\r') { + throw x.syntaxError("Missing close quote '" + q + "'."); + } + sb.append(c); + } + return sb.toString(); + case ',': + x.back(); + return ""; + default: + x.back(); + return x.nextTo(','); + } + } + + public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { + JSONArray ja = new JSONArray(); + for (;;) { + String value = getValue(x); + char c = x.next(); + if (value == null || + (ja.length() == 0 && value.length() == 0 && c != ',')) { + return null; + } + ja.put(value); + for (;;) { + if (c == ',') { + break; + } + if (c != ' ') { + if (c == '\n' || c == '\r' || c == 0) { + return ja; + } + throw x.syntaxError("Bad character '" + c + "' (" + + (int)c + ")."); + } + c = x.next(); + } + } + } + + public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) + throws JSONException { + JSONArray ja = rowToJSONArray(x); + return ja != null ? ja.toJSONObject(names) : null; + } + + public static String rowToString(JSONArray ja) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + if (i > 0) { + sb.append(','); + } + Object object = ja.opt(i); + if (object != null) { + String string = object.toString(); + if (string.length() > 0 && (string.indexOf(',') >= 0 || + string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || + string.indexOf(0) >= 0 || string.charAt(0) == '"')) { + sb.append('"'); + int length = string.length(); + for (int j = 0; j < length; j += 1) { + char c = string.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } else { + sb.append(string); + } + } + } + sb.append('\n'); + return sb.toString(); + } + + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new JSONTokener(string)); + } + + public static JSONArray toJSONArray(JSONTokener x) throws JSONException { + return toJSONArray(rowToJSONArray(x), x); + } + + public static JSONArray toJSONArray(JSONArray names, String string) + throws JSONException { + return toJSONArray(names, new JSONTokener(string)); + } + + public static JSONArray toJSONArray(JSONArray names, JSONTokener x) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (;;) { + JSONObject jo = rowToJSONObject(names, x); + if (jo == null) { + break; + } + ja.put(jo); + } + if (ja.length() == 0) { + return null; + } + return ja; + } + public static String toString(JSONArray ja) throws JSONException { + JSONObject jo = ja.optJSONObject(0); + if (jo != null) { + JSONArray names = jo.names(); + if (names != null) { + return rowToString(names) + toString(names, ja); + } + } + return null; + } + + public static String toString(JSONArray names, JSONArray ja) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ja.length(); i += 1) { + JSONObject jo = ja.optJSONObject(i); + if (jo != null) { + sb.append(rowToString(jo.toJSONArray(names))); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/reandroid/lib/json/Cookie.java b/src/main/java/com/reandroid/lib/json/Cookie.java new file mode 100644 index 0000000..5a1235d --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/Cookie.java @@ -0,0 +1,138 @@ +package com.reandroid.lib.json; + +import java.util.Locale; + +public class Cookie { + + public static String escape(String string) { + char c; + String s = string.trim(); + int length = s.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i += 1) { + c = s.charAt(i); + if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { + sb.append('%'); + sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); + sb.append(Character.forDigit((char)(c & 0x0f), 16)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + public static JSONObject toJSONObject(String string) { + final JSONObject jo = new JSONObject(); + String name; + Object value; + + + JSONTokener x = new JSONTokener(string); + + name = unescape(x.nextTo('=').trim()); + //per RFC6265, if the name is blank, the cookie should be ignored. + if("".equals(name)) { + throw new JSONException("Cookies must have a 'name'"); + } + jo.put("name", name); + // per RFC6265, if there is no '=', the cookie should be ignored. + // the 'next' call here throws an exception if the '=' is not found. + x.next('='); + jo.put("value", unescape(x.nextTo(';')).trim()); + // discard the ';' + x.next(); + // parse the remaining cookie attributes + while (x.more()) { + name = unescape(x.nextTo("=;")).trim().toLowerCase(Locale.ROOT); + // don't allow a cookies attributes to overwrite it's name or value. + if("name".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'name'"); + } + if("value".equalsIgnoreCase(name)) { + throw new JSONException("Illegal attribute name: 'value'"); + } + // check to see if it's a flag property + if (x.next() != '=') { + value = Boolean.TRUE; + } else { + value = unescape(x.nextTo(';')).trim(); + x.next(); + } + // only store non-blank attributes + if(!"".equals(name) && !"".equals(value)) { + jo.put(name, value); + } + } + return jo; + } + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + + String name = null; + Object value = null; + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key)) { + name = jo.getString(key).trim(); + } + if("value".equalsIgnoreCase(key)) { + value=jo.getString(key).trim(); + } + if(name != null && value != null) { + break; + } + } + + if(name == null || "".equals(name.trim())) { + throw new JSONException("Cookie does not have a name"); + } + if(value == null) { + value = ""; + } + + sb.append(escape(name)); + sb.append("="); + sb.append(escape((String)value)); + + for(String key : jo.keySet()){ + if("name".equalsIgnoreCase(key) + || "value".equalsIgnoreCase(key)) { + // already processed above + continue; + } + value = jo.opt(key); + if(value instanceof Boolean) { + if(Boolean.TRUE.equals(value)) { + sb.append(';').append(escape(key)); + } + // don't emit false values + } else { + sb.append(';') + .append(escape(key)) + .append('=') + .append(escape(value.toString())); + } + } + + return sb.toString(); + } + + public static String unescape(String string) { + int length = string.length(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + char c = string.charAt(i); + if (c == '+') { + c = ' '; + } else if (c == '%' && i + 2 < length) { + int d = JSONTokener.dehexchar(string.charAt(i + 1)); + int e = JSONTokener.dehexchar(string.charAt(i + 2)); + if (d >= 0 && e >= 0) { + c = (char)(d * 16 + e); + i += 2; + } + } + sb.append(c); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/reandroid/lib/json/CookieList.java b/src/main/java/com/reandroid/lib/json/CookieList.java new file mode 100644 index 0000000..846fa7e --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/CookieList.java @@ -0,0 +1,35 @@ +package com.reandroid.lib.json; + +public class CookieList { + + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + JSONTokener x = new JSONTokener(string); + while (x.more()) { + String name = Cookie.unescape(x.nextTo('=')); + x.next('='); + jo.put(name, Cookie.unescape(x.nextTo(';'))); + x.next(); + } + return jo; + } + + public static String toString(JSONObject jo) throws JSONException { + boolean b = false; + final StringBuilder sb = new StringBuilder(); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + final Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + if (b) { + sb.append(';'); + } + sb.append(Cookie.escape(key)); + sb.append("="); + sb.append(Cookie.escape(value.toString())); + b = true; + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/reandroid/lib/json/HTTP.java b/src/main/java/com/reandroid/lib/json/HTTP.java new file mode 100644 index 0000000..dafd6fb --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/HTTP.java @@ -0,0 +1,81 @@ +package com.reandroid.lib.json; + +import java.util.Locale; + +public class HTTP { + + /** Carriage return/line feed. */ + public static final String CRLF = "\r\n"; + + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + HTTPTokener x = new HTTPTokener(string); + String token; + + token = x.nextToken(); + if (token.toUpperCase(Locale.ROOT).startsWith("HTTP")) { + +// Response + + jo.put("HTTP-Version", token); + jo.put("Status-Code", x.nextToken()); + jo.put("Reason-Phrase", x.nextTo('\0')); + x.next(); + + } else { + +// Request + + jo.put("Method", token); + jo.put("Request-URI", x.nextToken()); + jo.put("HTTP-Version", x.nextToken()); + } + +// Fields + + while (x.more()) { + String name = x.nextTo(':'); + x.next(':'); + jo.put(name, x.nextTo('\0')); + x.next(); + } + return jo; + } + + + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { + sb.append(jo.getString("HTTP-Version")); + sb.append(' '); + sb.append(jo.getString("Status-Code")); + sb.append(' '); + sb.append(jo.getString("Reason-Phrase")); + } else if (jo.has("Method") && jo.has("Request-URI")) { + sb.append(jo.getString("Method")); + sb.append(' '); + sb.append('"'); + sb.append(jo.getString("Request-URI")); + sb.append('"'); + sb.append(' '); + sb.append(jo.getString("HTTP-Version")); + } else { + throw new JSONException("Not enough material for an HTTP header."); + } + sb.append(CRLF); + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + String value = jo.optString(key); + if (!"HTTP-Version".equals(key) && !"Status-Code".equals(key) && + !"Reason-Phrase".equals(key) && !"Method".equals(key) && + !"Request-URI".equals(key) && !JSONObject.NULL.equals(value)) { + sb.append(key); + sb.append(": "); + sb.append(jo.optString(key)); + sb.append(CRLF); + } + } + sb.append(CRLF); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/reandroid/lib/json/HTTPTokener.java b/src/main/java/com/reandroid/lib/json/HTTPTokener.java new file mode 100644 index 0000000..f406d18 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/HTTPTokener.java @@ -0,0 +1,36 @@ +package com.reandroid.lib.json; + +public class HTTPTokener extends JSONTokener { + + public HTTPTokener(String string) { + super(string); + } + public String nextToken() throws JSONException { + char c; + char q; + StringBuilder sb = new StringBuilder(); + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == '"' || c == '\'') { + q = c; + for (;;) { + c = next(); + if (c < ' ') { + throw syntaxError("Unterminated string."); + } + if (c == q) { + return sb.toString(); + } + sb.append(c); + } + } + for (;;) { + if (c == 0 || Character.isWhitespace(c)) { + return sb.toString(); + } + sb.append(c); + c = next(); + } + } +} diff --git a/src/main/java/com/reandroid/lib/json/JSONArray.java b/src/main/java/com/reandroid/lib/json/JSONArray.java new file mode 100644 index 0000000..8d52c70 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONArray.java @@ -0,0 +1,755 @@ +package com.reandroid.lib.json; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class JSONArray extends JSONItem implements Iterable { + + private final ArrayList myArrayList; + + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + public JSONArray(Collection collection) { + if (collection == null) { + this.myArrayList = new ArrayList(); + } else { + this.myArrayList = new ArrayList(collection.size()); + this.addAll(collection, true); + } + } + + public JSONArray(Iterable iter) { + this(); + if (iter == null) { + return; + } + this.addAll(iter, true); + } + + public JSONArray(JSONArray array) { + if (array == null) { + this.myArrayList = new ArrayList(); + } else { + // shallow copy directly the internal array lists as any wrapping + // should have been done already in the original JSONArray + this.myArrayList = new ArrayList(array.myArrayList); + } + } + + public JSONArray(Object array) throws JSONException { + this(); + if (!array.getClass().isArray()) { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + this.addAll(array, true); + } + + public JSONArray(int initialCapacity) throws JSONException { + if (initialCapacity < 0) { + throw new JSONException( + "JSONArray initial capacity cannot be negative."); + } + this.myArrayList = new ArrayList(initialCapacity); + } + + @Override + public Iterator iterator() { + return this.myArrayList.iterator(); + } + + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(index, "boolean", null); + } + + public double getDouble(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "double", e); + } + } + + public float getFloat(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Float)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "float", e); + } + } + + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "number", e); + } + } + + public > E getEnum(Class clazz, int index) throws JSONException { + E val = optEnum(clazz, index); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(index, "enum of type " + + JSONObject.quote(clazz.getSimpleName()), null); + } + return val; + } + + public BigDecimal getBigDecimal (int index) throws JSONException { + Object object = this.get(index); + BigDecimal val = JSONObject.objectToBigDecimal(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigDecimal", object, null); + } + return val; + } + + public BigInteger getBigInteger (int index) throws JSONException { + Object object = this.get(index); + BigInteger val = JSONObject.objectToBigInteger(object, null); + if(val == null) { + throw wrongValueFormatException(index, "BigInteger", object, null); + } + return val; + } + + public int getInt(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "int", e); + } + } + + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(index, "JSONArray", null); + } + + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(index, "JSONObject", null); + } + + public long getLong(int index) throws JSONException { + final Object object = this.get(index); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(index, "long", e); + } + } + + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(index, "String", null); + } + + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + public String join(String separator) throws JSONException { + int len = this.length(); + if (len == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder( + JSONObject.valueToString(this.myArrayList.get(0))); + + for (int i = 1; i < len; i++) { + sb.append(separator) + .append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + public int length() { + return this.myArrayList.size(); + } + + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList + .get(index); + } + + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + public double optDouble(int index, double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + public float optFloat(int index, float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + + public int optInt(int index) { + return this.optInt(index, 0); + } + + public int optInt(int index, int defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + public > E optEnum(Class clazz, int index) { + return this.optEnum(clazz, index, null); + } + + public > E optEnum(Class clazz, int index, E defaultValue) { + try { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigInteger(val, defaultValue); + } + + public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + return JSONObject.objectToBigDecimal(val, defaultValue); + } + + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + public long optLong(int index) { + return this.optLong(index, 0); + } + + public long optLong(int index, long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + public String optString(int index) { + return this.optString(index, ""); + } + + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object + .toString(); + } + + public JSONArray put(boolean value) { + return this.put(value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONArray put(Collection value) { + return this.put(new JSONArray(value)); + } + + public JSONArray put(double value) throws JSONException { + return this.put(Double.valueOf(value)); + } + + + public JSONArray put(float value) throws JSONException { + return this.put(Float.valueOf(value)); + } + + public JSONArray put(int value) { + return this.put(Integer.valueOf(value)); + } + + public JSONArray put(long value) { + return this.put(Long.valueOf(value)); + } + + public JSONArray put(Map value) { + return this.put(new JSONObject(value)); + } + + public JSONArray put(Object value) { + JSONObject.testValidity(value); + this.myArrayList.add(value); + return this; + } + + public JSONArray put(int index, boolean value) throws JSONException { + return this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONArray put(int index, Collection value) throws JSONException { + return this.put(index, new JSONArray(value)); + } + + public JSONArray put(int index, double value) throws JSONException { + return this.put(index, Double.valueOf(value)); + } + + public JSONArray put(int index, float value) throws JSONException { + return this.put(index, Float.valueOf(value)); + } + + public JSONArray put(int index, int value) throws JSONException { + return this.put(index, Integer.valueOf(value)); + } + + public JSONArray put(int index, long value) throws JSONException { + return this.put(index, Long.valueOf(value)); + } + + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + public JSONArray put(int index, Object value) throws JSONException { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + JSONObject.testValidity(value); + this.myArrayList.set(index, value); + return this; + } + if(index == this.length()){ + // simple append + return this.put(value); + } + // if we are inserting past the length, we want to grow the array all at once + // instead of incrementally. + this.myArrayList.ensureCapacity(index + 1); + while (index != this.length()) { + // we don't need to test validity of NULL objects + this.myArrayList.add(JSONObject.NULL); + } + return this.put(value); + } + + public JSONArray putAll(Collection collection) { + this.addAll(collection, false); + return this; + } + + + public JSONArray putAll(Iterable iter) { + this.addAll(iter, false); + return this; + } + + public JSONArray putAll(JSONArray array) { + // directly copy the elements from the source array to this one + // as all wrapping should have been done already in the source. + this.myArrayList.addAll(array.myArrayList); + return this; + } + + public JSONArray putAll(Object array) throws JSONException { + this.addAll(array, false); + return this; + } + + + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + public Object remove(int index) { + return index >= 0 && index < this.length() + ? this.myArrayList.remove(index) + : null; + } + + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray)other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.myArrayList.get(i); + Object valueOther = ((JSONArray)other).myArrayList.get(i); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.isEmpty() || this.isEmpty()) { + return null; + } + JSONObject jo = new JSONObject(names.length()); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean needsComma = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + try { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: 0", e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); + try { + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONArray value at index: " + i, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + public List toList() { + List results = new ArrayList(this.myArrayList.size()); + for (Object element : this.myArrayList) { + if (element == null || JSONObject.NULL.equals(element)) { + results.add(null); + } else if (element instanceof JSONArray) { + results.add(((JSONArray) element).toList()); + } else if (element instanceof JSONObject) { + results.add(((JSONObject) element).toMap()); + } else { + results.add(element); + } + } + return results; + } + + public boolean isEmpty() { + return this.myArrayList.isEmpty(); + } + + private void addAll(Collection collection, boolean wrap) { + this.myArrayList.ensureCapacity(this.myArrayList.size() + collection.size()); + if (wrap) { + for (Object o: collection){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: collection){ + this.put(o); + } + } + } + + private void addAll(Iterable iter, boolean wrap) { + if (wrap) { + for (Object o: iter){ + this.put(JSONObject.wrap(o)); + } + } else { + for (Object o: iter){ + this.put(o); + } + } + } + + + private void addAll(Object array, boolean wrap) throws JSONException { + if (array.getClass().isArray()) { + int length = Array.getLength(array); + this.myArrayList.ensureCapacity(this.myArrayList.size() + length); + if (wrap) { + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + for (int i = 0; i < length; i += 1) { + this.put(Array.get(array, i)); + } + } + } else if (array instanceof JSONArray) { + // use the built in array list `addAll` as all object + // wrapping should have been completed in the original + // JSONArray + this.myArrayList.addAll(((JSONArray)array).myArrayList); + } else if (array instanceof Collection) { + this.addAll((Collection)array, wrap); + } else if (array instanceof Iterable) { + this.addAll((Iterable)array, wrap); + } else { + throw new JSONException( + "JSONArray initial value should be a string or collection or array."); + } + } + + + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + "." + , cause); + } + + + private static JSONException wrongValueFormatException( + int idx, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONArray[" + idx + "] is not a " + valueType + " (" + value + ")." + , cause); + } + +} diff --git a/src/main/java/com/reandroid/lib/json/JsonItem.java b/src/main/java/com/reandroid/lib/json/JSONConvert.java similarity index 64% rename from src/main/java/com/reandroid/lib/json/JsonItem.java rename to src/main/java/com/reandroid/lib/json/JSONConvert.java index 6f8c402..ab62960 100644 --- a/src/main/java/com/reandroid/lib/json/JsonItem.java +++ b/src/main/java/com/reandroid/lib/json/JSONConvert.java @@ -1,6 +1,6 @@ package com.reandroid.lib.json; -public interface JsonItem { +public interface JSONConvert { public T toJson(); public void fromJson(T json); } diff --git a/src/main/java/com/reandroid/lib/json/JSONException.java b/src/main/java/com/reandroid/lib/json/JSONException.java new file mode 100644 index 0000000..206b12b --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONException.java @@ -0,0 +1,19 @@ +package com.reandroid.lib.json; + +public class JSONException extends IllegalArgumentException { + /** Serialization ID */ + private static final long serialVersionUID = 0; + + public JSONException(final String message) { + super(message); + } + + public JSONException(final String message, final Throwable cause) { + super(message, cause); + } + + public JSONException(final Throwable cause) { + super(cause.getMessage(), cause); + } + +} diff --git a/src/main/java/com/reandroid/lib/json/JSONItem.java b/src/main/java/com/reandroid/lib/json/JSONItem.java new file mode 100644 index 0000000..1dcaa9e --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONItem.java @@ -0,0 +1,49 @@ +package com.reandroid.lib.json; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public abstract class JSONItem { + public abstract Writer write(Writer writer, int indentFactor, int indent) throws JSONException; + + public void write(File file) throws IOException{ + write(file, INDENT_FACTOR); + } + public void write(File file, int indentFactor) throws IOException{ + File dir=file.getParentFile(); + if(dir!=null && !dir.exists()){ + dir.mkdirs(); + } + FileOutputStream outputStream=new FileOutputStream(file); + write(outputStream, indentFactor); + outputStream.close(); + } + public void write(OutputStream outputStream) throws IOException { + write(outputStream, INDENT_FACTOR); + } + public void write(OutputStream outputStream, int indentFactor) throws IOException { + Writer writer=new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writer= write(writer, indentFactor, 0); + writer.flush(); + writer.close(); + } + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + @Override + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + private static final int INDENT_FACTOR=1; +} diff --git a/src/main/java/com/reandroid/lib/json/JSONML.java b/src/main/java/com/reandroid/lib/json/JSONML.java new file mode 100644 index 0000000..c258e48 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONML.java @@ -0,0 +1,416 @@ +package com.reandroid.lib.json; + +public class JSONML { + + private static Object parse( + XMLTokener x, + boolean arrayForm, + JSONArray ja, + boolean keepStrings + ) throws JSONException { + String attribute; + char c; + String closeTag = null; + int i; + JSONArray newja = null; + JSONObject newjo = null; + Object token; + String tagName = null; + +// Test for and skip past these forms: +// +// +// +// + + while (true) { + if (!x.more()) { + throw x.syntaxError("Bad XML"); + } + token = x.nextContent(); + if (token == XML.LT) { + token = x.nextToken(); + if (token instanceof Character) { + if (token == XML.SLASH) { + +// Close tag "); + } else { + x.back(); + } + } else if (c == '[') { + token = x.nextToken(); + if (token.equals("CDATA") && x.next() == '[') { + if (ja != null) { + ja.put(x.nextCDATA()); + } + } else { + throw x.syntaxError("Expected 'CDATA['"); + } + } else { + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + } + } else if (token == XML.QUEST) { + +// "); + } else { + throw x.syntaxError("Misshaped tag"); + } + +// Open tag < + + } else { + if (!(token instanceof String)) { + throw x.syntaxError("Bad tagName '" + token + "'."); + } + tagName = (String)token; + newja = new JSONArray(); + newjo = new JSONObject(); + if (arrayForm) { + newja.put(tagName); + if (ja != null) { + ja.put(newja); + } + } else { + newjo.put("tagName", tagName); + if (ja != null) { + ja.put(newjo); + } + } + token = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (!(token instanceof String)) { + break; + } + +// attribute = value + + attribute = (String)token; + if (!arrayForm && ("tagName".equals(attribute) || "childNode".equals(attribute))) { + throw x.syntaxError("Reserved attribute."); + } + token = x.nextToken(); + if (token == XML.EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); + token = null; + } else { + newjo.accumulate(attribute, ""); + } + } + if (arrayForm && newjo.length() > 0) { + newja.put(newjo); + } + +// Empty tag <.../> + + if (token == XML.SLASH) { + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + +// Content, between <...> and + + } else { + if (token != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + closeTag = (String)parse(x, arrayForm, newja, keepStrings); + if (closeTag != null) { + if (!closeTag.equals(tagName)) { + throw x.syntaxError("Mismatched '" + tagName + + "' and '" + closeTag + "'"); + } + tagName = null; + if (!arrayForm && newja.length() > 0) { + newjo.put("childNodes", newja); + } + if (ja == null) { + if (arrayForm) { + return newja; + } + return newjo; + } + } + } + } + } else { + if (ja != null) { + ja.put(token instanceof String + ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) + : token); + } + } + } + } + public static JSONArray toJSONArray(String string) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, false); + } + public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { + return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); + } + public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONArray)parse(x, true, null, keepStrings); + } + public static JSONArray toJSONArray(XMLTokener x) throws JSONException { + return (JSONArray)parse(x, true, null, false); + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, false); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param string The XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x) throws JSONException { + return (JSONObject)parse(x, false, null, false); + } + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and
{@code <[ [ ]]>}
are ignored. + * @param x An XMLTokener of the XML source text. + * @param keepStrings If true, then values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { + return (JSONObject)parse(x, false, null, keepStrings); + } + public static String toString(JSONArray ja) throws JSONException { + int i; + JSONObject jo; + int length; + Object object; + StringBuilder sb = new StringBuilder(); + String tagName; + +// Emit = length) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + do { + object = ja.get(i); + i += 1; + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } while (i < length); + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } + + public static String toString(JSONObject jo) throws JSONException { + StringBuilder sb = new StringBuilder(); + int i; + JSONArray ja; + int length; + Object object; + String tagName; + Object value; + +//Emit '); + } else { + sb.append('>'); + length = ja.length(); + for (i = 0; i < length; i += 1) { + object = ja.get(i); + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/reandroid/lib/json/JSONObject.java b/src/main/java/com/reandroid/lib/json/JSONObject.java new file mode 100644 index 0000000..963a369 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONObject.java @@ -0,0 +1,1405 @@ +package com.reandroid.lib.json; + +import java.io.Closeable; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.regex.Pattern; + +public class JSONObject extends JSONItem{ + + private static final class Null { + + @Override + protected final Object clone() { + return this; + } + + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return "null"; + } + } + + + static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"); + + private final Map map; + + public static final Object NULL = new Null(); + + public JSONObject() { + // HashMap is used on purpose to ensure that elements are unordered by + // the specification. + // JSON tends to be a portable transfer format to allows the container + // implementations to rearrange their items for a faster element + // retrieval based on associative access. + // Therefore, an implementation mustn't rely on the order of the item. + this.map = new HashMap(); + } + + public JSONObject(JSONObject jo, String ... names) { + this(names.length); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + + // Use syntaxError(..) to include error location + + if (key != null) { + // Check if key exists + if (this.opt(key) != null) { + // key already exists + throw x.syntaxError("Duplicate key \"" + key + "\""); + } + // Only add value if non-null + Object value = x.nextValue(); + if (value!=null) { + this.put(key, value); + } + } + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + public JSONObject(Map m) { + if (m == null) { + this.map = new HashMap(); + } else { + this.map = new HashMap(m.size()); + for (final Entry e : m.entrySet()) { + if(e.getKey() == null) { + throw new NullPointerException("Null key."); + } + final Object value = e.getValue(); + if (value != null) { + this.map.put(String.valueOf(e.getKey()), wrap(value)); + } + } + } + } + + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + public JSONObject(Object object, String ... names) { + this(names.length); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + + protected JSONObject(int initialCapacity){ + this.map = new HashMap(initialCapacity); + } + + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw wrongValueFormatException(key, "JSONArray", null, null); + } + return this; + } + + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + public > E getEnum(Class clazz, String key) throws JSONException { + E val = optEnum(clazz, key); + if(val==null) { + // JSONException should really take a throwable argument. + // If it did, I would re-implement this with the Enum.valueOf + // method and place any thrown exception in the JSONException + throw wrongValueFormatException(key, "enum of type " + quote(clazz.getSimpleName()), null); + } + return val; + } + + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw wrongValueFormatException(key, "Boolean", null); + } + + public BigInteger getBigInteger(String key) throws JSONException { + Object object = this.get(key); + BigInteger ret = objectToBigInteger(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigInteger", object, null); + } + + public BigDecimal getBigDecimal(String key) throws JSONException { + Object object = this.get(key); + BigDecimal ret = objectToBigDecimal(object, null); + if (ret != null) { + return ret; + } + throw wrongValueFormatException(key, "BigDecimal", object, null); + } + + public double getDouble(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).doubleValue(); + } + try { + return Double.parseDouble(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "double", e); + } + } + + public float getFloat(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).floatValue(); + } + try { + return Float.parseFloat(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "float", e); + } + } + + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "number", e); + } + } + + public int getInt(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).intValue(); + } + try { + return Integer.parseInt(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "int", e); + } + } + + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw wrongValueFormatException(key, "JSONArray", null); + } + + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw wrongValueFormatException(key, "JSONObject", null); + } + + public long getLong(String key) throws JSONException { + final Object object = this.get(key); + if(object instanceof Number) { + return ((Number)object).longValue(); + } + try { + return Long.parseLong(object.toString()); + } catch (Exception e) { + throw wrongValueFormatException(key, "long", e); + } + } + + public static String[] getNames(JSONObject jo) { + if (jo.isEmpty()) { + return null; + } + return jo.keySet().toArray(new String[jo.length()]); + } + + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw wrongValueFormatException(key, "string", null); + } + + public boolean has(String key) { + return this.map.containsKey(key); + } + + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer) value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long) value).longValue() + 1L); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger)value).add(BigInteger.ONE)); + } else if (value instanceof Float) { + this.put(key, ((Float) value).floatValue() + 1.0f); + } else if (value instanceof Double) { + this.put(key, ((Double) value).doubleValue() + 1.0d); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal)value).add(BigDecimal.ONE)); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + public Iterator keys() { + return this.keySet().iterator(); + } + + public Set keySet() { + return this.map.keySet(); + } + + protected Set> entrySet() { + return this.map.entrySet(); + } + + public int length() { + return this.map.size(); + } + + public boolean isEmpty() { + return this.map.isEmpty(); + } + + public JSONArray names() { + if(this.map.isEmpty()) { + return null; + } + return new JSONArray(this.map.keySet()); + } + + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + public > E optEnum(Class clazz, String key) { + return this.optEnum(clazz, key, null); + } + + public > E optEnum(Class clazz, String key, E defaultValue) { + try { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (clazz.isAssignableFrom(val.getClass())) { + // we just checked it! + @SuppressWarnings("unchecked") + E myE = (E) val; + return myE; + } + return Enum.valueOf(clazz, val.toString()); + } catch (IllegalArgumentException e) { + return defaultValue; + } catch (NullPointerException e) { + return defaultValue; + } + } + + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + return objectToBigDecimal(val, defaultValue); + } + + static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + return objectToBigInteger(val, defaultValue); + } + + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + final double d = ((Number) val).doubleValue(); + if(Double.isNaN(d)) { + return defaultValue; + } + return new BigDecimal(d).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + public double optDouble(String key, double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + public float optFloat(String key, float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + + public int optInt(String key) { + return this.optInt(key, 0); + } + + public int optInt(String key, int defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + public long optLong(String key) { + return this.optLong(key, 0); + } + + public long optLong(String key, long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + + + public Number optNumber(String key) { + return this.optNumber(key, null); + } + + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + try { + return stringToNumber(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + + public String optString(String key) { + return this.optString(key, ""); + } + + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods(); + for (final Method method : methods) { + final int modifiers = method.getModifiers(); + if (Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && method.getParameterTypes().length == 0 + && !method.isBridge() + && method.getReturnType() != Void.TYPE + && isValidMethodName(method.getName())) { + final String key = getKeyNameFromMethod(method); + if (key != null && !key.isEmpty()) { + try { + final Object result = method.invoke(bean); + if (result != null) { + this.map.put(key, wrap(result)); + // we don't use the result anywhere outside of wrap + // if it's a resource we should be sure to close it + // after calling toString + if (result instanceof Closeable) { + try { + ((Closeable) result).close(); + } catch (IOException ignore) { + } + } + } + } catch (IllegalAccessException ignore) { + } catch (IllegalArgumentException ignore) { + } catch (InvocationTargetException ignore) { + } + } + } + } + } + + private static boolean isValidMethodName(String name) { + return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + } + + private static String getKeyNameFromMethod(Method method) { + final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); + if (ignoreDepth > 0) { + final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); + if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) { + // the hierarchy asked to ignore, and the nearest name override + // was higher or non-existent + return null; + } + } + JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class); + if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + String key; + final String name = method.getName(); + if (name.startsWith("get") && name.length() > 3) { + key = name.substring(3); + } else if (name.startsWith("is") && name.length() > 2) { + key = name.substring(2); + } else { + return null; + } + // if the first letter in the key is not uppercase, then skip. + // This is to maintain backwards compatibility before PR406 + // (https://github.com/stleary/JSON-java/pull/406/) + if (Character.isLowerCase(key.charAt(0))) { + return null; + } + if (key.length() == 1) { + key = key.toLowerCase(Locale.ROOT); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1); + } + return key; + } + + private static A getAnnotation(final Method m, final Class annotationClass) { + // if we have invalid data the result is null + if (m == null || annotationClass == null) { + return null; + } + + if (m.isAnnotationPresent(annotationClass)) { + return m.getAnnotation(annotationClass); + } + + // if we've already reached the Object class, return null; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return null; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + return getAnnotation(im, annotationClass); + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + return getAnnotation( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + } catch (final SecurityException ex) { + return null; + } catch (final NoSuchMethodException ex) { + return null; + } + } + + private static int getAnnotationDepth(final Method m, final Class annotationClass) { + // if we have invalid data the result is -1 + if (m == null || annotationClass == null) { + return -1; + } + + if (m.isAnnotationPresent(annotationClass)) { + return 1; + } + + // if we've already reached the Object class, return -1; + Class c = m.getDeclaringClass(); + if (c.getSuperclass() == null) { + return -1; + } + + // check directly implemented interfaces for the method being checked + for (Class i : c.getInterfaces()) { + try { + Method im = i.getMethod(m.getName(), m.getParameterTypes()); + int d = getAnnotationDepth(im, annotationClass); + if (d > 0) { + // since the annotation was on the interface, add 1 + return d + 1; + } + } catch (final SecurityException ex) { + continue; + } catch (final NoSuchMethodException ex) { + continue; + } + } + + try { + int d = getAnnotationDepth( + c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), + annotationClass); + if (d > 0) { + // since the annotation was on the superclass, add 1 + return d + 1; + } + return -1; + } catch (final SecurityException ex) { + return -1; + } catch (final NoSuchMethodException ex) { + return -1; + } + } + + public JSONObject put(String key, boolean value) throws JSONException { + return this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + } + + public JSONObject put(String key, Collection value) throws JSONException { + return this.put(key, new JSONArray(value)); + } + + public JSONObject put(String key, double value) throws JSONException { + return this.put(key, Double.valueOf(value)); + } + + + public JSONObject put(String key, float value) throws JSONException { + return this.put(key, Float.valueOf(value)); + } + + public JSONObject put(String key, int value) throws JSONException { + return this.put(key, Integer.valueOf(value)); + } + + public JSONObject put(String key, long value) throws JSONException { + return this.put(key, Long.valueOf(value)); + } + + public JSONObject put(String key, Map value) throws JSONException { + return this.put(key, new JSONObject(value)); + } + + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + return this.put(key, value); + } + return this; + } + + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + return this.put(key, value); + } + return this; + } + + public Object query(String jsonPointer) { + return query(new JSONPointer(jsonPointer)); + } + + public Object query(JSONPointer jsonPointer) { + return jsonPointer.queryFrom(this); + } + + + public Object optQuery(String jsonPointer) { + return optQuery(new JSONPointer(jsonPointer)); + } + + + public Object optQuery(JSONPointer jsonPointer) { + try { + return jsonPointer.queryFrom(this); + } catch (JSONPointerException e) { + return null; + } + } + + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.isEmpty()) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + public Object remove(String key) { + return this.map.remove(key); + } + + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + if (!this.keySet().equals(((JSONObject)other).keySet())) { + return false; + } + for (final Entry entry : this.entrySet()) { + String name = entry.getKey(); + Object valueThis = entry.getValue(); + Object valueOther = ((JSONObject)other).get(name); + if(valueThis == valueOther) { + continue; + } + if(valueThis == null) { + return false; + } + if (valueThis instanceof JSONObject) { + if (!((JSONObject)valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray)valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.isEmpty()) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + + public static String valueToString(Object value) throws JSONException { + // moves the implementation to JSONWriter as: + // 1. It makes more sense to be part of the writer class + // 2. For Android support this method is not available. By implementing it in the Writer + // Android users can use the writer with the built in Android JSONObject implementation. + return JSONWriter.valueToString(value); + } + + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String || object instanceof BigInteger + || object instanceof BigDecimal || object instanceof Enum) { + return object; + } + + if (object instanceof Collection) { + Collection coll = (Collection) object; + return new JSONArray(coll); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + Map map = (Map) object; + return new JSONObject(map); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary + final String numberAsString = numberToString((Number) value); + if(NUMBER_PATTERN.matcher(numberAsString).matches()) { + writer.write(numberAsString); + } else { + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + quote(numberAsString, writer); + } + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof Enum) { + writer.write(quote(((Enum)value).name())); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + Map map = (Map) value; + new JSONObject(map).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + new JSONArray(coll).write(writer, indentFactor, indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + try { + boolean needsComma = false; + final int length = this.length(); + writer.write('{'); + + if (length == 1) { + final Entry entry = this.entrySet().iterator().next(); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try{ + writeValue(writer, entry.getValue(), indentFactor, indent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + } else if (length != 0) { + final int newIndent = indent + indentFactor; + for (final Entry entry : this.entrySet()) { + if (needsComma) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newIndent); + final String key = entry.getKey(); + writer.write(quote(key)); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + try { + writeValue(writer, entry.getValue(), indentFactor, newIndent); + } catch (Exception e) { + throw new JSONException("Unable to write JSONObject value for key: " + key, e); + } + needsComma = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + public Map toMap() { + Map results = new HashMap(); + for (Entry entry : this.entrySet()) { + Object value; + if (entry.getValue() == null || NULL.equals(entry.getValue())) { + value = null; + } else if (entry.getValue() instanceof JSONObject) { + value = ((JSONObject) entry.getValue()).toMap(); + } else if (entry.getValue() instanceof JSONArray) { + value = ((JSONArray) entry.getValue()).toList(); + } else { + value = entry.getValue(); + } + results.put(entry.getKey(), value); + } + return results; + } + + + private static JSONException wrongValueFormatException( + String key, + String valueType, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + "." + , cause); + } + + + private static JSONException wrongValueFormatException( + String key, + String valueType, + Object value, + Throwable cause) { + return new JSONException( + "JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value + ")." + , cause); + } +} diff --git a/src/main/java/com/reandroid/lib/json/JSONPointer.java b/src/main/java/com/reandroid/lib/json/JSONPointer.java new file mode 100644 index 0000000..bdccff8 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONPointer.java @@ -0,0 +1,168 @@ +package com.reandroid.lib.json; + +import static java.lang.String.format; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class JSONPointer { + + // used for URL encoding and decoding + private static final String ENCODING = "utf-8"; + + public static class Builder { + + // Segments for the eventual JSONPointer string + private final List refTokens = new ArrayList(); + + public JSONPointer build() { + return new JSONPointer(this.refTokens); + } + + public Builder append(String token) { + if (token == null) { + throw new NullPointerException("token cannot be null"); + } + this.refTokens.add(token); + return this; + } + + public Builder append(int arrayIndex) { + this.refTokens.add(String.valueOf(arrayIndex)); + return this; + } + } + + public static Builder builder() { + return new Builder(); + } + + // Segments for the JSONPointer string + private final List refTokens; + + public JSONPointer(final String pointer) { + if (pointer == null) { + throw new NullPointerException("pointer cannot be null"); + } + if (pointer.isEmpty() || pointer.equals("#")) { + this.refTokens = Collections.emptyList(); + return; + } + String refs; + if (pointer.startsWith("#/")) { + refs = pointer.substring(2); + try { + refs = URLDecoder.decode(refs, ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } else if (pointer.startsWith("/")) { + refs = pointer.substring(1); + } else { + throw new IllegalArgumentException("a JSON pointer should start with '/' or '#/'"); + } + this.refTokens = new ArrayList(); + int slashIdx = -1; + int prevSlashIdx = 0; + do { + prevSlashIdx = slashIdx + 1; + slashIdx = refs.indexOf('/', prevSlashIdx); + if(prevSlashIdx == slashIdx || prevSlashIdx == refs.length()) { + // found 2 slashes in a row ( obj//next ) + // or single slash at the end of a string ( obj/test/ ) + this.refTokens.add(""); + } else if (slashIdx >= 0) { + final String token = refs.substring(prevSlashIdx, slashIdx); + this.refTokens.add(unescape(token)); + } else { + // last item after separator, or no separator at all. + final String token = refs.substring(prevSlashIdx); + this.refTokens.add(unescape(token)); + } + } while (slashIdx >= 0); + // using split does not take into account consecutive separators or "ending nulls" + //for (String token : refs.split("/")) { + // this.refTokens.add(unescape(token)); + //} + } + + public JSONPointer(List refTokens) { + this.refTokens = new ArrayList(refTokens); + } + + private static String unescape(String token) { + return token.replace("~1", "/").replace("~0", "~") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + + public Object queryFrom(Object document) throws JSONPointerException { + if (this.refTokens.isEmpty()) { + return document; + } + Object current = document; + for (String token : this.refTokens) { + if (current instanceof JSONObject) { + current = ((JSONObject) current).opt(unescape(token)); + } else if (current instanceof JSONArray) { + current = readByIndexToken(current, token); + } else { + throw new JSONPointerException(format( + "value [%s] is not an array or object therefore its key %s cannot be resolved", current, + token)); + } + } + return current; + } + + private static Object readByIndexToken(Object current, String indexToken) throws JSONPointerException { + try { + int index = Integer.parseInt(indexToken); + JSONArray currentArr = (JSONArray) current; + if (index >= currentArr.length()) { + throw new JSONPointerException(format("index %s is out of bounds - the array has %d elements", indexToken, + Integer.valueOf(currentArr.length()))); + } + try { + return currentArr.get(index); + } catch (JSONException e) { + throw new JSONPointerException("Error reading value at index position " + index, e); + } + } catch (NumberFormatException e) { + throw new JSONPointerException(format("%s is not an array index", indexToken), e); + } + } + + @Override + public String toString() { + StringBuilder rval = new StringBuilder(""); + for (String token: this.refTokens) { + rval.append('/').append(escape(token)); + } + return rval.toString(); + } + + private static String escape(String token) { + return token.replace("~", "~0") + .replace("/", "~1") + .replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + public String toURIFragment() { + try { + StringBuilder rval = new StringBuilder("#"); + for (String token : this.refTokens) { + rval.append('/').append(URLEncoder.encode(token, ENCODING)); + } + return rval.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/com/reandroid/lib/json/JSONPointerException.java b/src/main/java/com/reandroid/lib/json/JSONPointerException.java new file mode 100644 index 0000000..9de5a11 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONPointerException.java @@ -0,0 +1,14 @@ +package com.reandroid.lib.json; + +public class JSONPointerException extends JSONException { + private static final long serialVersionUID = 8872944667561856751L; + + public JSONPointerException(String message) { + super(message); + } + + public JSONPointerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/reandroid/lib/json/JSONPropertyIgnore.java b/src/main/java/com/reandroid/lib/json/JSONPropertyIgnore.java new file mode 100644 index 0000000..428cbe3 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONPropertyIgnore.java @@ -0,0 +1,14 @@ +package com.reandroid.lib.json; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) + +public @interface JSONPropertyIgnore { } diff --git a/src/main/java/com/reandroid/lib/json/JSONPropertyName.java b/src/main/java/com/reandroid/lib/json/JSONPropertyName.java new file mode 100644 index 0000000..f889c9a --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONPropertyName.java @@ -0,0 +1,17 @@ +package com.reandroid.lib.json; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({METHOD}) + +public @interface JSONPropertyName { + + String value(); +} diff --git a/src/main/java/com/reandroid/lib/json/JSONString.java b/src/main/java/com/reandroid/lib/json/JSONString.java new file mode 100644 index 0000000..5fa99aa --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONString.java @@ -0,0 +1,6 @@ +package com.reandroid.lib.json; + +public interface JSONString { + + public String toJSONString(); +} diff --git a/src/main/java/com/reandroid/lib/json/JSONStringer.java b/src/main/java/com/reandroid/lib/json/JSONStringer.java new file mode 100644 index 0000000..597ae41 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONStringer.java @@ -0,0 +1,15 @@ +package com.reandroid.lib.json; + +import java.io.StringWriter; + +public class JSONStringer extends JSONWriter { + + public JSONStringer() { + super(new StringWriter()); + } + + @Override + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/src/main/java/com/reandroid/lib/json/JSONTokener.java b/src/main/java/com/reandroid/lib/json/JSONTokener.java new file mode 100644 index 0000000..5175d76 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONTokener.java @@ -0,0 +1,339 @@ +package com.reandroid.lib.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +public class JSONTokener { + /** current read character position on the current line. */ + private long character; + /** flag to indicate if the end of the input has been found. */ + private boolean eof; + /** current read index of the input. */ + private long index; + /** current line of the input. */ + private long line; + /** previous character read from the input. */ + private char previous; + /** Reader for the input. */ + private final Reader reader; + /** flag to indicate that a previous character was requested. */ + private boolean usePrevious; + /** the number of characters read in the previous line. */ + private long characterPreviousLine; + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.characterPreviousLine = 0; + this.line = 1; + } + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + public JSONTokener(String s) { + this(new StringReader(s)); + } + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.decrementIndexes(); + this.usePrevious = true; + this.eof = false; + } + + private void decrementIndexes() { + this.index--; + if(this.previous=='\r' || this.previous == '\n') { + this.line--; + this.character=this.characterPreviousLine ; + } else if(this.character > 0){ + this.character--; + } + } + + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return this.eof && !this.usePrevious; + } + public boolean more() throws JSONException { + if(this.usePrevious) { + return true; + } + try { + this.reader.mark(1); + } catch (IOException e) { + throw new JSONException("Unable to preserve stream position", e); + } + try { + // -1 is EOF, but next() can not consume the null character '\0' + if(this.reader.read() <= 0) { + this.eof = true; + return false; + } + this.reader.reset(); + } catch (IOException e) { + throw new JSONException("Unable to read the next character from the stream", e); + } + return true; + } + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + if (c <= 0) { // End of stream + this.eof = true; + return 0; + } + this.incrementIndexes(c); + this.previous = (char) c; + return this.previous; + } + + private void incrementIndexes(int c) { + if(c > 0) { + this.index++; + if(c=='\r') { + this.line++; + this.characterPreviousLine = this.character; + this.character=0; + }else if (c=='\n') { + if(this.previous != '\r') { + this.line++; + this.characterPreviousLine = this.character; + } + this.character=0; + } else { + this.character++; + } + } + } + + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + if(n > 0) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + throw this.syntaxError("Expected '" + c + "' and instead saw ''"); + } + return n; + } + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + try { + sb.append((char)Integer.parseInt(this.next(4), 16)); + } catch (NumberFormatException e) { + throw this.syntaxError("Illegal escape.", e); + } + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + if (!this.eof) { + this.back(); + } + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + // in some readers, reset() may throw an exception if + // the remaining portion of the input is greater than + // the mark size (1,000,000 above). + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return 0; + } + } while (c != to); + this.reader.mark(1); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + public JSONException syntaxError(String message, Throwable causedBy) { + return new JSONException(message + this.toString(), causedBy); + } + + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/src/main/java/com/reandroid/lib/json/JSONWriter.java b/src/main/java/com/reandroid/lib/json/JSONWriter.java new file mode 100644 index 0000000..4cc309b --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/JSONWriter.java @@ -0,0 +1,219 @@ +package com.reandroid.lib.json; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +public class JSONWriter { + private static final int maxdepth = 200; + + private boolean comma; + + protected char mode; + + private final JSONObject stack[]; + + private int top; + + protected Appendable writer; + + public JSONWriter(Appendable w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.append(','); + } + this.writer.append(string); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + private JSONWriter end(char m, char c) throws JSONException { + if (this.mode != m) { + throw new JSONException(m == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(m); + try { + this.writer.append(c); + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + this.comma = true; + return this; + } + + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + JSONObject topObject = this.stack[this.top - 1]; + // don't use the built in putOnce method to maintain Android support + if(topObject.has(string)) { + throw new JSONException("Duplicate key \"" + string + "\""); + } + topObject.put(string, true); + if (this.comma) { + this.writer.append(','); + } + this.writer.append(JSONObject.quote(string)); + this.writer.append(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + // Android as of API 25 does not support this exception constructor + // however we won't worry about it. If an exception is happening here + // it will just throw a "Method not found" exception instead. + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + String object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object != null) { + return object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex + final String numberAsString = JSONObject.numberToString((Number) value); + if(JSONObject.NUMBER_PATTERN.matcher(numberAsString).matches()) { + // Close enough to a JSON number that we will return it unquoted + return numberAsString; + } + // The Number value is not a valid JSON number. + // Instead we will quote it as a string + return JSONObject.quote(numberAsString); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } + if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + if(value instanceof Enum){ + return JSONObject.quote(((Enum)value).name()); + } + return JSONObject.quote(value.toString()); + } + + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + public JSONWriter value(double d) throws JSONException { + return this.value(Double.valueOf(d)); + } + + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + public JSONWriter value(Object object) throws JSONException { + return this.append(valueToString(object)); + } +} diff --git a/src/main/java/com/reandroid/lib/json/JsonUtil.java b/src/main/java/com/reandroid/lib/json/JsonUtil.java index db46d5c..27ff1d3 100644 --- a/src/main/java/com/reandroid/lib/json/JsonUtil.java +++ b/src/main/java/com/reandroid/lib/json/JsonUtil.java @@ -1,62 +1,36 @@ package com.reandroid.lib.json; -import org.json.JSONArray; -import org.json.JSONObject; - import java.io.*; import java.nio.charset.StandardCharsets; public class JsonUtil { - public static void writeJSONObject(JsonItem jsonItem, File file) throws IOException{ - writeJSONObject(jsonItem, file, INDENT_FACTOR); + + public static void readJSONObject(File file, JSONConvert jsonConvert) throws IOException { + FileInputStream inputStream=new FileInputStream(file); + readJSONObject(inputStream, jsonConvert); + inputStream.close(); } - public static void writeJSONObject(JsonItem jsonItem, File file, int indentFactor) throws IOException{ - File dir=file.getParentFile(); - if(dir!=null && !dir.exists()){ - dir.mkdirs(); - } - FileOutputStream outputStream=new FileOutputStream(file); - writeJSONObject(jsonItem, outputStream, indentFactor); + public static void readJSONObject(InputStream inputStream, JSONConvert jsonConvert){ + InputStreamReader reader=new InputStreamReader(inputStream, StandardCharsets.UTF_8); + readJSONObject(reader, jsonConvert); } - public static void writeJSONObject(JsonItem jsonItem, OutputStream outputStream) throws IOException{ - writeJSONObject(jsonItem, outputStream, INDENT_FACTOR); - } - public static void writeJSONObject(JsonItem jsonItem, OutputStream outputStream, int indentFactor) throws IOException{ - Writer writer=new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - writer= writeJSONObject(jsonItem, writer, indentFactor); - writer.flush(); - writer.close(); - } - public static Writer writeJSONObject(JsonItem jsonItem, Writer writer, int indentFactor){ - JSONObject jsonObject=jsonItem.toJson(); - return jsonObject.write(writer, indentFactor, 0); + public static void readJSONObject(Reader reader, JSONConvert jsonConvert){ + JSONObject jsonObject=new JSONObject(new JSONTokener(reader)); + jsonConvert.fromJson(jsonObject); } - - public static void writeJSONArray(JsonItem jsonItem, File file) throws IOException{ - writeJSONArray(jsonItem, file, INDENT_FACTOR); + public static void readJSONArray(File file, JSONConvert jsonConvert) throws IOException { + FileInputStream inputStream=new FileInputStream(file); + readJSONArray(inputStream, jsonConvert); + inputStream.close(); } - public static void writeJSONArray(JsonItem jsonItem, File file, int indentFactor) throws IOException{ - File dir=file.getParentFile(); - if(dir!=null && !dir.exists()){ - dir.mkdirs(); - } - FileOutputStream outputStream=new FileOutputStream(file); - writeJSONArray(jsonItem, outputStream, indentFactor); + public static void readJSONArray(InputStream inputStream, JSONConvert jsonConvert){ + InputStreamReader reader=new InputStreamReader(inputStream, StandardCharsets.UTF_8); + readJSONArray(reader, jsonConvert); } - public static void writeJSONArray(JsonItem jsonItem, OutputStream outputStream) throws IOException{ - writeJSONArray(jsonItem, outputStream, INDENT_FACTOR); - } - public static void writeJSONArray(JsonItem jsonItem, OutputStream outputStream, int indentFactor) throws IOException{ - Writer writer=new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - writer= writeJSONArray(jsonItem, writer, indentFactor); - writer.flush(); - writer.close(); - } - public static Writer writeJSONArray(JsonItem jsonItem, Writer writer, int indentFactor){ - JSONArray jsonArray=jsonItem.toJson(); - return jsonArray.write(writer, indentFactor, 0); + public static void readJSONArray(Reader reader, JSONConvert jsonConvert){ + JSONArray jsonObject=new JSONArray(new JSONTokener(reader)); + jsonConvert.fromJson(jsonObject); } - private static final int INDENT_FACTOR=4; } diff --git a/src/main/java/com/reandroid/lib/json/Property.java b/src/main/java/com/reandroid/lib/json/Property.java new file mode 100644 index 0000000..e944a05 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/Property.java @@ -0,0 +1,35 @@ +package com.reandroid.lib.json; + +import java.util.Enumeration; +import java.util.Properties; + +public class Property { + + public static JSONObject toJSONObject(java.util.Properties properties) throws JSONException { + // can't use the new constructor for Android support + // JSONObject jo = new JSONObject(properties == null ? 0 : properties.size()); + JSONObject jo = new JSONObject(); + if (properties != null && !properties.isEmpty()) { + Enumeration enumProperties = properties.propertyNames(); + while(enumProperties.hasMoreElements()) { + String name = (String)enumProperties.nextElement(); + jo.put(name, properties.getProperty(name)); + } + } + return jo; + } + + public static Properties toProperties(JSONObject jo) throws JSONException { + Properties properties = new Properties(); + if (jo != null) { + // Don't use the new entrySet API to maintain Android support + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (!JSONObject.NULL.equals(value)) { + properties.put(key, value.toString()); + } + } + } + return properties; + } +} diff --git a/src/main/java/com/reandroid/lib/json/XML.java b/src/main/java/com/reandroid/lib/json/XML.java new file mode 100644 index 0000000..a9cf30e --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/XML.java @@ -0,0 +1,600 @@ +package com.reandroid.lib.json; + +import java.io.Reader; +import java.io.StringReader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; +@SuppressWarnings("boxing") +public class XML { + + /** The Character '&'. */ + public static final Character AMP = '&'; + + /** The Character '''. */ + public static final Character APOS = '\''; + + /** The Character '!'. */ + public static final Character BANG = '!'; + + /** The Character '='. */ + public static final Character EQ = '='; + + /** The Character
{@code '>'. }
*/ + public static final Character GT = '>'; + + /** The Character '<'. */ + public static final Character LT = '<'; + + /** The Character '?'. */ + public static final Character QUEST = '?'; + + /** The Character '"'. */ + public static final Character QUOT = '"'; + + /** The Character '/'. */ + public static final Character SLASH = '/'; + + public static final String NULL_ATTR = "xsi:nil"; + + public static final String TYPE_ATTR = "xsi:type"; + + private static Iterable codePointIterator(final String string) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private int nextIndex = 0; + private int length = string.length(); + + @Override + public boolean hasNext() { + return this.nextIndex < this.length; + } + + @Override + public Integer next() { + int result = string.codePointAt(this.nextIndex); + this.nextIndex += Character.charCount(result); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + public static String escape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (final int cp : codePointIterator(string)) { + switch (cp) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + if (mustEscape(cp)) { + sb.append("&#x"); + sb.append(Integer.toHexString(cp)); + sb.append(';'); + } else { + sb.appendCodePoint(cp); + } + } + } + return sb.toString(); + } + + private static boolean mustEscape(int cp) { + /* Valid range from https://www.w3.org/TR/REC-xml/#charsets + * + * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * + * any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. + */ + // isISOControl is true when (cp >= 0 && cp <= 0x1F) || (cp >= 0x7F && cp <= 0x9F) + // all ISO control characters are out of range except tabs and new lines + return (Character.isISOControl(cp) + && cp != 0x9 + && cp != 0xA + && cp != 0xD + ) || !( + // valid the range of acceptable characters that aren't control + (cp >= 0x20 && cp <= 0xD7FF) + || (cp >= 0xE000 && cp <= 0xFFFD) + || (cp >= 0x10000 && cp <= 0x10FFFF) + ) + ; + } + + public static String unescape(String string) { + StringBuilder sb = new StringBuilder(string.length()); + for (int i = 0, length = string.length(); i < length; i++) { + char c = string.charAt(i); + if (c == '&') { + final int semic = string.indexOf(';', i); + if (semic > i) { + final String entity = string.substring(i + 1, semic); + sb.append(XMLTokener.unescapeEntity(entity)); + // skip past the entity we just parsed. + i += entity.length() + 1; + } else { + // this shouldn't happen in most cases since the parser + // errors on unclosed entries. + sb.append(c); + } + } else { + // not part of an entity + sb.append(c); + } + } + return sb.toString(); + } + + public static void noSpace(String string) throws JSONException { + int i, length = string.length(); + if (length == 0) { + throw new JSONException("Empty string."); + } + for (i = 0; i < length; i += 1) { + if (Character.isWhitespace(string.charAt(i))) { + throw new JSONException("'" + string + + "' contains a space character."); + } + } + } + + private static boolean parse(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config) + throws JSONException { + char c; + int i; + JSONObject jsonObject = null; + String string; + String tagName; + Object token; + XMLXsiTypeConverter xmlXsiTypeConverter; + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate(config.getcDataTagName(), string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (nilAttributeFound) { + context.accumulate(tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(tagName, jsonObject); + } else { + context.accumulate(tagName, ""); + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + if(xmlXsiTypeConverter != null) { + jsonObject.accumulate(config.getcDataTagName(), + stringToValue(string, xmlXsiTypeConverter)); + } else { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepStrings() ? string : stringToValue(string)); + } + } + + } else if (token == LT) { + // Nested element + if (parse(x, jsonObject, tagName, config)) { + if (jsonObject.length() == 0) { + context.accumulate(tagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(tagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.accumulate(tagName, jsonObject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + public static Object stringToValue(String string, XMLXsiTypeConverter typeConverter) { + if(typeConverter != null) { + return typeConverter.convert(string); + } + return stringToValue(string); + } + + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. + // This method should not make calls out of the XML object. + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + + + private static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // Use a BigDecimal all the time so we keep the original + // representation. BigDecimal doesn't support -0.0, ensure we + // keep that by forcing a decimal. + try { + BigDecimal bd = new BigDecimal(val); + if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) { + return Double.valueOf(-0.0); + } + return bd; + } catch (NumberFormatException retryAsDouble) { + // this is to support "Hex Floats" like this: 0x1.0P-1074 + try { + Double d = Double.valueOf(val); + if(d.isNaN() || d.isInfinite()) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + return d; + } catch (NumberFormatException ignore) { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + } + // block items like 00 01 etc. Java number parsers treat these as Octal. + if(initial == '0' && val.length() > 1) { + char at1 = val.charAt(1); + if(at1 >= '0' && at1 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } else if (initial == '-' && val.length() > 2) { + char at1 = val.charAt(1); + char at2 = val.charAt(2); + if(at1 == '0' && at2 >= '0' && at2 <= '9') { + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // BigInteger down conversion: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. + BigInteger bi = new BigInteger(val); + if(bi.bitLength() <= 31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength() <= 63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } + + + private static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(string, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader) throws JSONException { + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws JSONException { + if(keepStrings) { + return toJSONObject(reader, XMLParserConfiguration.KEEP_STRINGS); + } + return toJSONObject(reader, XMLParserConfiguration.ORIGINAL); + } + + public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration config) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(reader); + while (x.more()) { + x.skipPast("<"); + if(x.more()) { + parse(x, jo, null, config); + } + } + return jo; + } + + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + return toJSONObject(new StringReader(string), keepStrings); + } + + public static JSONObject toJSONObject(String string, XMLParserConfiguration config) throws JSONException { + return toJSONObject(new StringReader(string), config); + } + + public static String toString(Object object) throws JSONException { + return toString(object, null, XMLParserConfiguration.ORIGINAL); + } + + public static String toString(final Object object, final String tagName) { + return toString(object, tagName, XMLParserConfiguration.ORIGINAL); + } + + public static String toString(final Object object, final String tagName, final XMLParserConfiguration config) + throws JSONException { + StringBuilder sb = new StringBuilder(); + JSONArray ja; + JSONObject jo; + String string; + + if (object instanceof JSONObject) { + + // Emit + if (tagName != null) { + sb.append('<'); + sb.append(tagName); + sb.append('>'); + } + + // Loop thru the keys. + // don't use the new entrySet accessor to maintain Android Support + jo = (JSONObject) object; + for (final String key : jo.keySet()) { + Object value = jo.opt(key); + if (value == null) { + value = ""; + } else if (value.getClass().isArray()) { + value = new JSONArray(value); + } + + // Emit content in body + if (key.equals(config.getcDataTagName())) { + if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + if (i > 0) { + sb.append('\n'); + } + Object val = ja.opt(i); + sb.append(escape(val.toString())); + } + } else { + sb.append(escape(value.toString())); + } + + // Emit an array of similar keys + + } else if (value instanceof JSONArray) { + ja = (JSONArray) value; + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + if (val instanceof JSONArray) { + sb.append('<'); + sb.append(key); + sb.append('>'); + sb.append(toString(val, null, config)); + sb.append("'); + } else { + sb.append(toString(val, key, config)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + + // Emit a new tag + + } else { + sb.append(toString(value, key, config)); + } + } + if (tagName != null) { + + // Emit the close tag + sb.append("'); + } + return sb.toString(); + + } + + if (object != null && (object instanceof JSONArray || object.getClass().isArray())) { + if(object.getClass().isArray()) { + ja = new JSONArray(object); + } else { + ja = (JSONArray) object; + } + int jaLength = ja.length(); + // don't use the new iterator API to maintain support for Android + for (int i = 0; i < jaLength; i++) { + Object val = ja.opt(i); + // XML does not have good support for arrays. If an array + // appears in a place where XML is lacking, synthesize an + // element. + sb.append(toString(val, tagName == null ? "array" : tagName, config)); + } + return sb.toString(); + } + + string = (object == null) ? "null" : escape(object.toString()); + return (tagName == null) ? "\"" + string + "\"" + : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName + + ">" + string + ""; + + } +} diff --git a/src/main/java/com/reandroid/lib/json/XMLParserConfiguration.java b/src/main/java/com/reandroid/lib/json/XMLParserConfiguration.java new file mode 100644 index 0000000..83423ee --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/XMLParserConfiguration.java @@ -0,0 +1,120 @@ +package com.reandroid.lib.json; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +@SuppressWarnings({""}) +public class XMLParserConfiguration { + /** Original Configuration of the XML Parser. */ + public static final XMLParserConfiguration ORIGINAL + = new XMLParserConfiguration(); + /** Original configuration of the XML Parser except that values are kept as strings. */ + public static final XMLParserConfiguration KEEP_STRINGS + = new XMLParserConfiguration().withKeepStrings(true); + + private boolean keepStrings; + + + private String cDataTagName; + + + private boolean convertNilAttributeToNull; + + private Map> xsiTypeMap; + + public XMLParserConfiguration () { + this.keepStrings = false; + this.cDataTagName = "content"; + this.convertNilAttributeToNull = false; + this.xsiTypeMap = Collections.emptyMap(); + } + + @Deprecated + public XMLParserConfiguration (final boolean keepStrings) { + this(keepStrings, "content", false); + } + + @Deprecated + public XMLParserConfiguration (final String cDataTagName) { + this(false, cDataTagName, false); + } + + @Deprecated + public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = false; + } + + @Deprecated + public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + } + + private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, + final boolean convertNilAttributeToNull, final Map> xsiTypeMap ) { + this.keepStrings = keepStrings; + this.cDataTagName = cDataTagName; + this.convertNilAttributeToNull = convertNilAttributeToNull; + this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap); + } + + @Override + protected XMLParserConfiguration clone() { + // future modifications to this method should always ensure a "deep" + // clone in the case of collections. i.e. if a Map is added as a configuration + // item, a new map instance should be created and if possible each value in the + // map should be cloned as well. If the values of the map are known to also + // be immutable, then a shallow clone of the map is acceptable. + return new XMLParserConfiguration( + this.keepStrings, + this.cDataTagName, + this.convertNilAttributeToNull, + this.xsiTypeMap + ); + } + + + public boolean isKeepStrings() { + return this.keepStrings; + } + + public XMLParserConfiguration withKeepStrings(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.keepStrings = newVal; + return newConfig; + } + + public String getcDataTagName() { + return this.cDataTagName; + } + + public XMLParserConfiguration withcDataTagName(final String newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.cDataTagName = newVal; + return newConfig; + } + + public boolean isConvertNilAttributeToNull() { + return this.convertNilAttributeToNull; + } + + public XMLParserConfiguration withConvertNilAttributeToNull(final boolean newVal) { + XMLParserConfiguration newConfig = this.clone(); + newConfig.convertNilAttributeToNull = newVal; + return newConfig; + } + + public Map> getXsiTypeMap() { + return this.xsiTypeMap; + } + + public XMLParserConfiguration withXsiTypeMap(final Map> xsiTypeMap) { + XMLParserConfiguration newConfig = this.clone(); + Map> cloneXsiTypeMap = new HashMap>(xsiTypeMap); + newConfig.xsiTypeMap = Collections.unmodifiableMap(cloneXsiTypeMap); + return newConfig; + } +} diff --git a/src/main/java/com/reandroid/lib/json/XMLTokener.java b/src/main/java/com/reandroid/lib/json/XMLTokener.java new file mode 100644 index 0000000..03b43c0 --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/XMLTokener.java @@ -0,0 +1,312 @@ +package com.reandroid.lib.json; + +import java.io.Reader; + +public class XMLTokener extends JSONTokener { + + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + public XMLTokener(Reader r) { + super(r); + } + + public XMLTokener(String s) { + super(s); + } + + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuilder sb = new StringBuilder(); + while (more()) { + c = next(); + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && + sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + throw syntaxError("Unclosed CDATA"); + } + public Object nextContent() throws JSONException { + char c; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuilder(); + for (;;) { + if (c == 0) { + return sb.toString().trim(); + } + if (c == '<') { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + public Object nextEntity(@SuppressWarnings("unused") char ampersand) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String string = sb.toString(); + return unescapeEntity(string); + } + + + static String unescapeEntity(String e) { + // validate + if (e == null || e.isEmpty()) { + return ""; + } + // if our entity is an encoded unicode point, parse it. + if (e.charAt(0) == '#') { + int cp; + if (e.charAt(1) == 'x' || e.charAt(1) == 'X') { + // hex encoded unicode + cp = Integer.parseInt(e.substring(2), 16); + } else { + // decimal encoded unicode + cp = Integer.parseInt(e.substring(1)); + } + return new String(new int[] {cp},0,1); + } + Character knownEntity = entity.get(e); + if(knownEntity==null) { + // we don't know the entity so keep it encoded + return '&' + e + ';'; + } + return knownEntity.toString(); + } + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + throw syntaxError("Unterminated string"); + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + public Object nextToken() throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + +// Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + +// Name + + sb = new StringBuilder(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + // The Android implementation of JSONTokener has a public method of public void skipPast(String to) + // even though ours does not have that method, to have API compatibility, our method in the subclass + // should match. + public void skipPast(String to) { + boolean b; + char c; + int i; + int j; + int offset = 0; + int length = to.length(); + char[] circle = new char[length]; + + /* + * First fill the circle buffer with as many characters as are in the + * to string. If we reach an early end, bail. + */ + + for (i = 0; i < length; i += 1) { + c = next(); + if (c == 0) { + return; + } + circle[i] = c; + } + + /* We will loop, possibly for all of the remaining characters. */ + + for (;;) { + j = offset; + b = true; + + /* Compare the circle buffer with the to string. */ + + for (i = 0; i < length; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= length) { + j -= length; + } + } + + /* If we exit the loop with b intact, then victory is ours. */ + + if (b) { + return; + } + + /* Get the next character. If there isn't one, then defeat is ours. */ + + c = next(); + if (c == 0) { + return; + } + /* + * Shove the character in the circle buffer and advance the + * circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= length) { + offset -= length; + } + } + } +} diff --git a/src/main/java/com/reandroid/lib/json/XMLXsiTypeConverter.java b/src/main/java/com/reandroid/lib/json/XMLXsiTypeConverter.java new file mode 100644 index 0000000..fbf25bb --- /dev/null +++ b/src/main/java/com/reandroid/lib/json/XMLXsiTypeConverter.java @@ -0,0 +1,5 @@ +package com.reandroid.lib.json; + +public interface XMLXsiTypeConverter { + T convert(String value); +}