
com.reandroid.arsc.chunk.xml.ResXmlDocument Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ARSCLib Show documentation
Show all versions of ARSCLib Show documentation
Android binary resources read/write library
The newest version!
/*
* Copyright (C) 2022 github.com/REAndroid
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.reandroid.arsc.chunk.xml;
import com.reandroid.archive.InputSource;
import com.reandroid.arsc.ApkFile;
import com.reandroid.arsc.chunk.*;
import com.reandroid.arsc.container.BlockList;
import com.reandroid.arsc.header.HeaderBlock;
import com.reandroid.arsc.header.InfoHeader;
import com.reandroid.arsc.io.BlockReader;
import com.reandroid.arsc.pool.ResXmlStringPool;
import com.reandroid.arsc.pool.StringPool;
import com.reandroid.arsc.refactor.ResourceMergeOption;
import com.reandroid.common.BytesOutputStream;
import com.reandroid.json.JSONConvert;
import com.reandroid.json.JSONObject;
import com.reandroid.utils.collection.CollectionUtil;
import com.reandroid.utils.collection.IterableIterator;
import com.reandroid.utils.collection.SingleIterator;
import com.reandroid.xml.XMLDocument;
import com.reandroid.xml.XMLFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.*;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
public class ResXmlDocument extends Chunk
implements ResXmlNodeTree, MainChunk, ParentChunk, JSONConvert {
private final ResXmlStringPool mResXmlStringPool;
private final ResXmlIDMap mResXmlIDMap;
private final BlockList mNodeList;
private ApkFile mApkFile;
private PackageBlock mPackageBlock;
private boolean mDestroyed;
public ResXmlDocument() {
super(new HeaderBlock(ChunkType.XML),3);
this.mResXmlStringPool = new ResXmlStringPool(true);
this.mResXmlIDMap = new ResXmlIDMap();
this.mNodeList = new BlockList<>();
addChild(mResXmlStringPool);
addChild(mResXmlIDMap);
addChild(mNodeList);
this.mNodeList.add(new ResXmlElement());
}
@Override
public BlockList getNodeListBlockInternal() {
return mNodeList;
}
/**
* Iterates every attribute on root element and on child elements recursively
* */
public Iterator recursiveAttributes() throws ConcurrentModificationException{
return new IterableIterator(getElements()) {
@Override
public Iterator iterator(ResXmlElement element) {
return element.recursiveAttributes();
}
};
}
/**
* Iterates every xml node and child node recursively
* */
public Iterator recursiveXmlNodes() throws ConcurrentModificationException{
return new IterableIterator(iterator()) {
@Override
public Iterator iterator(ResXmlNode resXmlNode) {
if(resXmlNode instanceof ResXmlElement) {
return ((ResXmlElement) resXmlNode).recursiveXmlNodes();
}
return SingleIterator.of(resXmlNode);
}
};
}
/**
* Iterates every element and child elements recursively
* */
public Iterator recursiveElements() throws ConcurrentModificationException{
return new IterableIterator(getElements()) {
@Override
public Iterator iterator(ResXmlElement element) {
return element.recursiveElements();
}
};
}
public int autoSetAttributeNamespaces() {
return autoSetAttributeNamespaces(true);
}
public int autoSetAttributeNamespaces(boolean removeNoIdPrefix) {
int changedCount = 0;
Iterator iterator = getElements();
while (iterator.hasNext()) {
ResXmlElement element = iterator.next();
changedCount += element.autoSetAttributeNamespaces(removeNoIdPrefix);
}
if(changedCount > 0){
removeUnusedNamespaces();
getStringPool().removeUnusedStrings();
}
return changedCount;
}
public int autoSetAttributeNames() {
return autoSetAttributeNames(true);
}
public int autoSetAttributeNames(boolean removeNoIdPrefix) {
int changedCount = 0;
Iterator iterator = getElements();
while (iterator.hasNext()) {
changedCount += iterator.next().autoSetAttributeNames(removeNoIdPrefix);
}
if(changedCount > 0){
removeUnusedNamespaces();
getStringPool().removeUnusedStrings();
}
return changedCount;
}
public void autoSetLineNumber(){
Iterator iterator = getElements();
while (iterator.hasNext()) {
iterator.next().autoSetLineNumber();
}
}
public int removeUnusedNamespaces() {
int count = 0;
Iterator iterator = getElements();
while (iterator.hasNext()) {
count += iterator.next().removeUnusedNamespaces();
}
return count;
}
public String refreshFull(){
int sizeOld = getHeaderBlock().getChunkSize();
StringBuilder message = new StringBuilder();
boolean appendOnce = false;
int count;
getStringPool().compressDuplicates();
Iterator iterator = getElements();
while (iterator.hasNext()){
ResXmlElement element = iterator.next();
count = element.removeUndefinedAttributes();
if(count != 0){
message.append("Removed undefined attributes = ");
message.append(count);
appendOnce = true;
}
}
count = removeUnusedNamespaces();
if(count != 0){
if(appendOnce){
message.append("\n");
}
message.append("Removed unused namespaces = ");
message.append(count);
appendOnce = true;
}
if(getStringPool().removeUnusedStrings()){
if(appendOnce){
message.append("\n");
}
message.append("Removed unused xml strings");
appendOnce = true;
}
refresh();
int sizeNew = getHeaderBlock().getChunkSize();
if(sizeOld != sizeNew){
if(appendOnce){
message.append("\n");
}
message.append("Xml size changed = ");
message.append(sizeOld);
message.append(", ");
message.append(sizeNew);
appendOnce = true;
}
if(appendOnce){
return message.toString();
}
return null;
}
public void destroy(){
synchronized (this){
if(mDestroyed){
return;
}
mDestroyed = true;
mNodeList.clearChildes();
getResXmlIDMap().destroy();
getStringPool().clear();
refresh();
}
}
public void setAttributesUnitSize(int size, boolean setToAll){
Iterator iterator = getElements();
while (iterator.hasNext()) {
iterator.next().setAttributesUnitSize(size, setToAll);
}
}
public ResXmlElement getOrCreateElement(String tag){
ResXmlElement element = getElement(tag);
if(element == null){
element = createRootElement(tag);
}else if(tag != null){
element.setName(tag);
}
return element;
}
public ResXmlElement createRootElement(String tag){
int lineNo = 1;
ResXmlElement resXmlElement = newElement();
resXmlElement.newStartElement(lineNo);
if(tag != null){
resXmlElement.setName(tag);
}
return resXmlElement;
}
void linkStringReferences(){
Iterator iterator = getElements();
while (iterator.hasNext()) {
iterator.next().linkStringReferences();
}
}
@Override
public byte[] getBytes(){
BytesOutputStream outputStream = new BytesOutputStream(
getHeaderBlock().getChunkSize());
try {
writeBytes(outputStream);
outputStream.close();
} catch (IOException ignored) {
}
return outputStream.toByteArray();
}
@Override
public void onReadBytes(BlockReader reader) throws IOException {
HeaderBlock headerBlock = reader.readHeaderBlock();
if(headerBlock == null){
throw new IOException("Not bin xml: " + reader);
}
int chunkSize = headerBlock.getChunkSize();
if(chunkSize < 0){
throw new IOException("Negative chunk size: " + chunkSize);
}
if(chunkSize > reader.available()){
throw new IOException("Higher chunk size: " + chunkSize
+ ", available = " + reader.available());
}
if(chunkSize < headerBlock.getHeaderSize()){
throw new IOException("Higher header size: " + headerBlock);
}
BlockReader chunkReader = reader.create(chunkSize);
headerBlock = getHeaderBlock();
headerBlock.readBytes(chunkReader);
// android/aapt2 accepts 0x0000 (NULL) chunk type as XML, it could
// be android's bug and might be fixed in the future until then lets fix it ourselves
headerBlock.setType(ChunkType.XML);
clear();
while (chunkReader.isAvailable()){
boolean readOk = readNext(chunkReader);
if(!readOk){
break;
}
}
reader.offset(headerBlock.getChunkSize());
chunkReader.close();
onChunkLoaded();
}
@Override
public void onChunkLoaded(){
super.onChunkLoaded();
linkStringReferences();
}
private boolean readNext(BlockReader reader) throws IOException {
if(!reader.isAvailable()){
return false;
}
int position=reader.getPosition();
HeaderBlock headerBlock=reader.readHeaderBlock();
if(headerBlock==null){
return false;
}
ChunkType chunkType=headerBlock.getChunkType();
if(chunkType==ChunkType.STRING){
mResXmlStringPool.readBytes(reader);
}else if(chunkType==ChunkType.XML_RESOURCE_MAP){
mResXmlIDMap.readBytes(reader);
}else if(isElementChunk(chunkType)){
newElement().readBytes(reader);
return reader.isAvailable();
}else {
throw new IOException("Unexpected chunk "+headerBlock);
}
return reader.isAvailable() && position!=reader.getPosition();
}
private boolean isElementChunk(ChunkType chunkType){
if(chunkType==ChunkType.XML_START_ELEMENT){
return true;
}
if(chunkType==ChunkType.XML_END_ELEMENT){
return true;
}
if(chunkType==ChunkType.XML_START_NAMESPACE){
return true;
}
if(chunkType==ChunkType.XML_END_NAMESPACE){
return true;
}
if(chunkType==ChunkType.XML_CDATA){
return true;
}
if(chunkType==ChunkType.XML_LAST_CHUNK){
return true;
}
return false;
}
@Override
public ResXmlStringPool getStringPool(){
return mResXmlStringPool;
}
@Override
public ApkFile getApkFile(){
return mApkFile;
}
@Override
public void setApkFile(ApkFile apkFile){
this.mApkFile = apkFile;
}
@Override
public PackageBlock getPackageBlock(){
ApkFile apkFile = this.mApkFile;
PackageBlock packageBlock = this.mPackageBlock;
if(apkFile == null || packageBlock != null){
return packageBlock;
}
TableBlock tableBlock = apkFile.getLoadedTableBlock();
if(tableBlock != null){
packageBlock = selectPackageBlock(tableBlock);
mPackageBlock = packageBlock;
}
return packageBlock;
}
public void setPackageBlock(PackageBlock packageBlock) {
this.mPackageBlock = packageBlock;
}
PackageBlock selectPackageBlock(TableBlock tableBlock){
PackageBlock packageBlock = tableBlock.pickOne();
if(packageBlock == null){
packageBlock = tableBlock.pickOrEmptyPackage();
}
return packageBlock;
}
@Override
public TableBlock getTableBlock(){
PackageBlock packageBlock = getPackageBlock();
if(packageBlock != null){
TableBlock tableBlock = packageBlock.getTableBlock();
if(tableBlock != null){
return tableBlock;
}
}
ApkFile apkFile = getApkFile();
if(apkFile != null){
return apkFile.getLoadedTableBlock();
}
return null;
}
@Override
public StringPool> getSpecStringPool() {
return null;
}
@Override
public MainChunk getMainChunk(){
return this;
}
public ResXmlIDMap getResXmlIDMap(){
return mResXmlIDMap;
}
public ResXmlElement getDocumentElement(){
return CollectionUtil.getFirst(getElements());
}
public ResXmlElement newElement() {
clearEmptyElements();
ResXmlElement element = new ResXmlElement();
add(element);
return element;
}
public void addElement(int index, ResXmlElement element) {
clearEmptyElements();
this.add(index, element);
}
public void clearEmptyElements() {
this.removeIf(xmlNode -> {
if(xmlNode instanceof ResXmlElement) {
return ((ResXmlElement) xmlNode).isUndefined();
}
return false;
});
}
@Override
protected void onPreRefresh(){
clearEmptyElements();
getNodeListBlockInternal().refresh();
super.onPreRefresh();
}
@Override
protected void onChunkRefreshed() {
}
public void readBytes(File file) throws IOException{
BlockReader reader=new BlockReader(file);
super.readBytes(reader);
}
public void readBytes(InputStream inputStream) throws IOException{
BlockReader reader=new BlockReader(inputStream);
super.readBytes(reader);
}
public final int writeBytes(File file) throws IOException{
if(isNull()){
throw new IOException("Can NOT save null block");
}
File dir=file.getParentFile();
if(dir!=null && !dir.exists()){
dir.mkdirs();
}
OutputStream outputStream=new FileOutputStream(file);
int length = super.writeBytes(outputStream);
outputStream.close();
return length;
}
public void mergeWithName(ResourceMergeOption mergeOption, ResXmlDocument document) {
if(document == this){
return;
}
Iterator iterator = document.getElements();
while (iterator.hasNext()){
ResXmlElement coming = iterator.next();
ResXmlElement element = getOrCreateElement(coming.getName());
element.mergeWithName(mergeOption, coming);
}
}
public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
if(mDestroyed){
throw new IOException("Destroyed document");
}
PackageBlock packageBlock = getPackageBlock();
if(packageBlock == null){
throw new IOException("Can not decode without package");
}
setPackageBlock(packageBlock);
int event = parser.getEventType();
if(event == XmlPullParser.START_DOCUMENT){
clear();
parser.next();
}
while (parseNext(parser)){
parser.next();
}
refreshFull();
}
private boolean parseNext(XmlPullParser parser) throws IOException, XmlPullParserException {
int event = parser.getEventType();
while (event != XmlPullParser.START_TAG && event != XmlPullParser.END_DOCUMENT){
event = parser.next();
}
if(event == XmlPullParser.START_TAG){
newElement().parse(parser);
return true;
}
return false;
}
public String serializeToXml() throws IOException {
StringWriter writer = new StringWriter();
XmlSerializer serializer = XMLFactory.newSerializer(writer);
serialize(serializer);
serializer.flush();
writer.flush();
writer.close();
return writer.toString();
}
public void serialize(XmlSerializer serializer) throws IOException {
if(mDestroyed){
throw new IOException("Destroyed document");
}
PackageBlock packageBlock = getPackageBlock();
if(packageBlock == null){
throw new IOException("Can not decode without package");
}
ResXmlElement.setIndent(serializer, true);
serializer.startDocument("utf-8", null);
autoSetAttributeNamespaces();
Iterator iterator = getElements();
while (iterator.hasNext()){
iterator.next().serialize(serializer);
}
serializer.endDocument();
}
@Override
public JSONObject toJson() {
JSONObject jsonObject = new JSONObject();
jsonObject.put(ResXmlDocument.NAME_element, getDocumentElement().toJson());
return jsonObject;
}
@Override
public void fromJson(JSONObject json) {
ResXmlElement xmlElement = getDocumentElement();
xmlElement.fromJson(json.optJSONObject(ResXmlDocument.NAME_element));
refresh();
}
public XMLDocument decodeToXml() {
return toXml(true);
}
public XMLDocument toXml() {
return toXml(false);
}
public XMLDocument toXml(boolean decode) {
XMLDocument xmlDocument = new XMLDocument();
xmlDocument.setEncoding("utf-8");
for (ResXmlNode node : this) {
xmlDocument.add(node.toXml(decode));
}
return xmlDocument;
}
void addEvents(ParserEventList parserEventList){
ResXmlElement xmlElement = getDocumentElement();
parserEventList.add(new ParserEvent(ParserEvent.START_DOCUMENT, xmlElement));
Iterator iterator = getElements();
while (iterator.hasNext()) {
ResXmlElement element = iterator.next();
element.addEvents(parserEventList);
}
parserEventList.add(new ParserEvent(ParserEvent.END_DOCUMENT, xmlElement));
}
public static boolean isResXmlBlock(File file){
if(file==null){
return false;
}
try {
InfoHeader infoHeader = InfoHeader.readHeaderBlock(file);
return isResXmlBlock(infoHeader);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(InputSource inputSource) {
boolean result = false;
try {
InputStream inputStream = inputSource.openStream();
result = isResXmlBlock(inputStream);
inputStream.close();
} catch (IOException ignored) {
}
return result;
}
public static boolean isResXmlBlock(InputStream inputStream) {
try {
HeaderBlock headerBlock = BlockReader.readHeaderBlock(inputStream);
return isResXmlBlock(headerBlock);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(byte[] bytes){
try {
HeaderBlock headerBlock = BlockReader.readHeaderBlock(bytes);
return isResXmlBlock(headerBlock);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(BlockReader blockReader){
if(blockReader==null){
return false;
}
try {
HeaderBlock headerBlock = blockReader.readHeaderBlock();
return isResXmlBlock(headerBlock);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(HeaderBlock headerBlock){
if(headerBlock==null){
return false;
}
ChunkType chunkType=headerBlock.getChunkType();
return chunkType==ChunkType.XML;
}
private static final String NAME_element = "element";
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy