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 a9eab7a..0000000 Binary files a/libs/json.jar and /dev/null differ 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); +}