/* * 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.arsc.chunk.ChunkType; import com.reandroid.arsc.base.Block; import com.reandroid.arsc.container.BlockList; import com.reandroid.arsc.container.SingleBlockContainer; import com.reandroid.arsc.header.HeaderBlock; import com.reandroid.arsc.io.BlockReader; import com.reandroid.arsc.pool.ResXmlStringPool; import com.reandroid.common.EntryStore; import com.reandroid.json.JSONConvert; import com.reandroid.json.JSONArray; import com.reandroid.json.JSONObject; import com.reandroid.xml.*; import java.io.IOException; import java.util.*; public class ResXmlElement extends ResXmlNode implements JSONConvert, Comparator { private final BlockList mStartNamespaceList; private final SingleBlockContainer mStartElementContainer; private final BlockList mBody; private final SingleBlockContainer mEndElementContainer; private final BlockList mEndNamespaceList; private int mLevel; public ResXmlElement() { super(5); this.mStartNamespaceList = new BlockList<>(); this.mStartElementContainer= new SingleBlockContainer<>(); this.mBody = new BlockList<>(); this.mEndElementContainer = new SingleBlockContainer<>(); this.mEndNamespaceList = new BlockList<>(); addChild(0, mStartNamespaceList); addChild(1, mStartElementContainer); addChild(2, mBody); addChild(3, mEndElementContainer); addChild(4, mEndNamespaceList); } public void changeIndex(ResXmlElement element, int index){ int i = 0; for(ResXmlNode xmlNode:mBody.getChildes()){ if(i == index){ element.setIndex(i); i++; } if(xmlNode==element){ continue; } xmlNode.setIndex(i); i++; } mBody.sort(this); } public int lastIndexOf(String tagName){ List elementList = listElements(tagName); int i = elementList.size(); if(i==0){ return -1; } i--; return elementList.get(i).getIndex(); } public int indexOf(String tagName){ ResXmlElement element = getElementByTagName(tagName); if(element!=null){ return element.getIndex(); } return -1; } public int indexOf(ResXmlElement element){ int index = 0; for(ResXmlNode xmlNode:mBody.getChildes()){ if(xmlNode==element){ return index; } index++; } return -1; } public void setAttributesUnitSize(int size, boolean setToAll){ ResXmlStartElement startElement = getStartElement(); startElement.setAttributesUnitSize(size); if(setToAll){ for(ResXmlElement child:listElements()){ child.setAttributesUnitSize(size, setToAll); } } } public String getStartComment(){ ResXmlStartElement start = getStartElement(); if(start!=null){ return start.getComment(); } return null; } public String getEndComment(){ ResXmlEndElement end = getEndElement(); if(end!=null){ return end.getComment(); } return null; } public int getStartLineNumber(){ ResXmlStartElement start = getStartElement(); if(start!=null){ return start.getLineNumber(); } return 0; } public int getEndLineNumber(){ ResXmlEndElement end = getEndElement(); if(end!=null){ return end.getLineNumber(); } return 0; } public void setComment(String comment){ getStartElement().setComment(comment); } public void calculatePositions(){ ResXmlStartElement start = getStartElement(); if(start!=null){ start.calculatePositions(); } } public ResXmlAttribute newAttribute(){ return getStartElement().newAttribute(); } void onRemoved(){ for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ startNamespace.onRemoved(); } ResXmlStartElement start = getStartElement(); if(start!=null){ start.onRemoved(); } ResXmlText resXmlText=getResXmlText(); if(resXmlText!=null){ resXmlText.onRemoved(); } for(ResXmlElement child:listElements()){ child.onRemoved(); } } void linkStringReferences(){ for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ startNamespace.linkStringReferences(); } ResXmlStartElement start = getStartElement(); if(start!=null){ start.linkStringReferences(); } ResXmlText resXmlText=getResXmlText(); if(resXmlText!=null){ resXmlText.linkStringReferences(); } for(ResXmlElement child:listElements()){ child.linkStringReferences(); } } public ResXmlElement createChildElement(){ return createChildElement(null); } public ResXmlElement createChildElement(String tag){ int lineNo=getStartElement().getLineNumber()+1; ResXmlElement resXmlElement=new ResXmlElement(); resXmlElement.newStartElement(lineNo); addElement(resXmlElement); if(tag!=null){ resXmlElement.setTag(tag); } return resXmlElement; } public ResXmlAttribute getOrCreateAndroidAttribute(String name, int resourceId){ return getOrCreateAttribute(NS_ANDROID_URI, NS_ANDROID_PREFIX, name, resourceId); } public ResXmlAttribute getOrCreateAttribute(String uri, String prefix, String name, int resourceId){ ResXmlAttribute attribute=searchAttribute(name, resourceId); if(attribute==null){ attribute = createAttribute(name, resourceId); if(uri!=null){ ResXmlElement root = getRootResXmlElement(); ResXmlStartNamespace ns = root.getOrCreateNamespace(uri, prefix); attribute.setNamespaceReference(ns.getUriReference()); } } return attribute; } public ResXmlAttribute getOrCreateAttribute(String name, int resourceId){ ResXmlAttribute attribute=searchAttribute(name, resourceId); if(attribute==null){ attribute=createAttribute(name, resourceId); } return attribute; } public ResXmlAttribute createAndroidAttribute(String name, int resourceId){ ResXmlAttribute attribute=createAttribute(name, resourceId); ResXmlStartNamespace ns = getOrCreateNamespace(NS_ANDROID_URI, NS_ANDROID_PREFIX); attribute.setNamespaceReference(ns.getUriReference()); return attribute; } public ResXmlAttribute createAttribute(String name, int resourceId){ ResXmlAttribute attribute=new ResXmlAttribute(); addAttribute(attribute); attribute.setName(name, resourceId); return attribute; } public void addAttribute(ResXmlAttribute attribute){ getStartElement().getResXmlAttributeArray().add(attribute); } public ResXmlElement getElementByTagName(String name){ if(name==null){ return null; } for(ResXmlElement child:listElements()){ if(name.equals(child.getTag())||name.equals(child.getTagName())){ return child; } } return null; } private ResXmlAttribute searchAttribute(String name, int resourceId){ if(resourceId==0){ return searchAttributeByName(name); } return searchAttributeByResourceId(resourceId); } // Searches attribute with resource id = 0 public ResXmlAttribute searchAttributeByName(String name){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.searchAttributeByName(name); } return null; } public ResXmlAttribute searchAttributeByResourceId(int resourceId){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.searchAttributeByResourceId(resourceId); } return null; } public void setTag(String tag){ ResXmlStringPool pool = getStringPool(); if(pool==null){ return; } ensureStartEndElement(); ResXmlStartElement start=getStartElement(); String prefix=null; String name=tag; int i=tag.lastIndexOf(':'); if(i>=0){ prefix=tag.substring(0,i); i++; name=tag.substring(i); } start.setName(name); ResXmlStartNamespace ns = getStartNamespaceByPrefix(prefix); if(ns!=null){ start.setNamespaceReference(ns.getUriReference()); } } public String getTagName(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getTagName(); } return null; } public String getTag(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getName(); } return null; } public String getTagUri(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getUri(); } return null; } public String getTagPrefix(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getPrefix(); } return null; } public int getAttributeCount() { ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getResXmlAttributeArray().childesCount(); } return 0; } public ResXmlAttribute getAttributeAt(int index){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.getResXmlAttributeArray().get(index); } return null; } public Collection listAttributes(){ ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ return startElement.listResXmlAttributes(); } return new ArrayList<>(); } public ResXmlStringPool getStringPool(){ Block parent=getParent(); while (parent!=null){ if(parent instanceof ResXmlDocument){ return ((ResXmlDocument)parent).getStringPool(); } if(parent instanceof ResXmlElement){ return ((ResXmlElement)parent).getStringPool(); } parent=parent.getParent(); } return null; } public ResXmlIDMap getResXmlIDMap(){ ResXmlDocument resXmlDocument = getParentDocument(); if(resXmlDocument!=null){ return resXmlDocument.getResXmlIDMap(); } return null; } public ResXmlDocument getParentDocument(){ return getParentInstance(ResXmlDocument.class); } @Override public int getDepth(){ int depth = 0; ResXmlElement parent = getParentResXmlElement(); while (parent!=null){ depth++; parent = parent.getParentResXmlElement(); } return depth; } @Override void addEvents(ParserEventList parserEventList){ String comment = getStartComment(); if(comment!=null){ parserEventList.add( new ParserEvent(ParserEvent.COMMENT, this, comment, false)); } parserEventList.add(new ParserEvent(ParserEvent.START_TAG, this)); for(ResXmlNode xmlNode:getXmlNodes()){ xmlNode.addEvents(parserEventList); } comment = getEndComment(); if(comment!=null){ parserEventList.add( new ParserEvent(ParserEvent.COMMENT, this, comment, true)); } parserEventList.add(new ParserEvent(ParserEvent.END_TAG, this)); } public int getLevel(){ return mLevel; } private void setLevel(int level){ mLevel = level; } public void addElement(ResXmlElement element){ mBody.add(element); } public boolean removeAttribute(ResXmlAttribute resXmlAttribute){ if(resXmlAttribute != null){ resXmlAttribute.onRemoved(); } return getStartElement().getResXmlAttributeArray().remove(resXmlAttribute); } public boolean removeElement(ResXmlElement element){ if(element !=null && element.getParent()!=null){ element.onRemoved(); } return mBody.remove(element); } public boolean removeNode(ResXmlNode node){ if(node instanceof ResXmlElement){ return removeElement((ResXmlElement) node); } return mBody.remove(node); } public int countElements(){ int result = 0; for(ResXmlNode xmlNode: getXmlNodes()){ if(xmlNode instanceof ResXmlElement){ result++; } } return result; } public void clearChildes(){ List copyOfNodeList=new ArrayList<>(mBody.getChildes()); for(ResXmlNode xmlNode:copyOfNodeList){ if(xmlNode==null){ continue; } xmlNode.onRemove(); mBody.remove(xmlNode); } } public ResXmlNode getResXmlNode(int position){ return mBody.get(position); } public int countResXmlNodes(){ return mBody.size(); } public List listXmlNodes(){ return new ArrayList<>(getXmlNodes()); } private List getXmlNodes(){ return mBody.getChildes(); } public List listXmlText(){ List results=new ArrayList<>(); for(ResXmlNode xmlNode: getXmlNodes()){ if(xmlNode instanceof ResXmlTextNode){ results.add(((ResXmlTextNode) xmlNode).getResXmlText()); } } return results; } public List listXmlTextNodes(){ List results=new ArrayList<>(); for(ResXmlNode xmlNode: getXmlNodes()){ if(xmlNode instanceof ResXmlTextNode){ results.add((ResXmlTextNode) xmlNode); } } return results; } public List listElements(){ List results=new ArrayList<>(); for(ResXmlNode xmlNode: getXmlNodes()){ if(xmlNode instanceof ResXmlElement){ results.add((ResXmlElement) xmlNode); } } return results; } public List listElements(String name){ List results=new ArrayList<>(); if(name==null){ return results; } for(ResXmlElement element:listElements()){ if(name.equals(element.getTag())||name.equals(element.getTagName())){ results.add(element); } } return results; } public ResXmlElement getRootResXmlElement(){ ResXmlElement parent=getParentResXmlElement(); if(parent!=null){ return parent.getRootResXmlElement(); } return this; } public ResXmlElement getParentResXmlElement(){ return getParentInstance(ResXmlElement.class); } public ResXmlStartNamespace getStartNamespaceByUriRef(int uriRef){ if(uriRef<0){ return null; } for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ if(uriRef==ns.getUriReference()){ return ns; } } ResXmlElement xmlElement=getParentResXmlElement(); if(xmlElement!=null){ return xmlElement.getStartNamespaceByUriRef(uriRef); } return null; } public ResXmlStartNamespace getOrCreateNamespace(String uri, String prefix){ ResXmlStartNamespace exist=getStartNamespaceByUri(uri); if(exist!=null){ return exist; } ResXmlStartNamespace startNamespace=new ResXmlStartNamespace(); ResXmlEndNamespace endNamespace=new ResXmlEndNamespace(); startNamespace.setEnd(endNamespace); addStartNamespace(startNamespace); addEndNamespace(endNamespace); startNamespace.setUri(uri); startNamespace.setPrefix(prefix); return startNamespace; } public ResXmlStartNamespace getStartNamespaceByUri(String uri){ if(uri==null){ return null; } for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ if(uri.equals(ns.getUri())){ return ns; } } ResXmlElement xmlElement=getParentResXmlElement(); if(xmlElement!=null){ return xmlElement.getStartNamespaceByUri(uri); } return null; } public ResXmlStartNamespace getStartNamespaceByPrefix(String prefix){ if(prefix==null){ return null; } for(ResXmlStartNamespace ns:mStartNamespaceList.getChildes()){ if(prefix.equals(ns.getPrefix())){ return ns; } } ResXmlElement xmlElement=getParentResXmlElement(); if(xmlElement!=null){ return xmlElement.getStartNamespaceByPrefix(prefix); } return null; } public List getStartNamespaceList(){ return mStartNamespaceList.getChildes(); } public void addStartNamespace(ResXmlStartNamespace item){ mStartNamespaceList.add(item); } public List getEndNamespaceList(){ return mEndNamespaceList.getChildes(); } public void addEndNamespace(ResXmlEndNamespace item){ mEndNamespaceList.add(item); } ResXmlStartElement newStartElement(int lineNo){ ResXmlStartElement startElement=new ResXmlStartElement(); setStartElement(startElement); ResXmlEndElement endElement=new ResXmlEndElement(); startElement.setResXmlEndElement(endElement); setEndElement(endElement); endElement.setResXmlStartElement(startElement); startElement.setLineNumber(lineNo); endElement.setLineNumber(lineNo); return startElement; } public ResXmlStartElement getStartElement(){ return mStartElementContainer.getItem(); } public void setStartElement(ResXmlStartElement item){ mStartElementContainer.setItem(item); } public ResXmlEndElement getEndElement(){ return mEndElementContainer.getItem(); } public void setEndElement(ResXmlEndElement item){ mEndElementContainer.setItem(item); } // Use listXmlText() instead to be removed on next version @Deprecated public ResXmlText getResXmlText(){ List xmlTextList=listXmlText(); if(xmlTextList.size()==0){ return null; } return xmlTextList.get(0); } public void addResXmlTextNode(ResXmlTextNode xmlTextNode){ mBody.add(xmlTextNode); } public void addResXmlText(ResXmlText xmlText){ if(xmlText!=null){ addResXmlTextNode(new ResXmlTextNode(xmlText)); } } // Use addResXmlText() @Deprecated public void setResXmlText(ResXmlText xmlText){ addResXmlText(xmlText); } @Deprecated public void setResXmlText(String text){ clearChildes(); addResXmlText(text); } public void addResXmlText(String text){ if(text==null){ return; } ResXmlTextNode xmlTextNode=new ResXmlTextNode(); addResXmlTextNode(xmlTextNode); xmlTextNode.setText(text); } private boolean isBalanced(){ return isElementBalanced() && isNamespaceBalanced(); } private boolean isNamespaceBalanced(){ return (mStartNamespaceList.size()==mEndNamespaceList.size()); } private boolean isElementBalanced(){ return (hasStartElement() && hasEndElement()); } private boolean hasStartElement(){ return mStartElementContainer.hasItem(); } private boolean hasEndElement(){ return mEndElementContainer.hasItem(); } private void linkStartEnd(){ linkStartEndElement(); linkStartEndNameSpaces(); } private void linkStartEndElement(){ ResXmlStartElement start=getStartElement(); ResXmlEndElement end=getEndElement(); if(start==null || end==null){ return; } start.setResXmlEndElement(end); end.setResXmlStartElement(start); } private void ensureStartEndElement(){ ResXmlStartElement start=getStartElement(); ResXmlEndElement end=getEndElement(); if(start!=null && end!=null){ return; } if(start==null){ start=new ResXmlStartElement(); setStartElement(start); } if(end==null){ end=new ResXmlEndElement(); setEndElement(end); } linkStartEndElement(); } private void linkStartEndNameSpaces(){ if(!isNamespaceBalanced()){ return; } int max=mStartNamespaceList.size(); for(int i=0;i0 && getLevel()==0){ onFinishedUnexpected(reader); return; } onFinishedSuccess(reader, headerBlock); } private void onFinishedSuccess(BlockReader reader, HeaderBlock headerBlock) throws IOException{ } private void onFinishedUnexpected(BlockReader reader) throws IOException{ StringBuilder builder=new StringBuilder(); builder.append("Unexpected finish reading: reader=").append(reader.toString()); HeaderBlock header = reader.readHeaderBlock(); if(header!=null){ builder.append(", next header="); builder.append(header.toString()); } throw new IOException(builder.toString()); } private void onStartElement(BlockReader reader) throws IOException{ if(hasStartElement()){ ResXmlElement childElement=new ResXmlElement(); addElement(childElement); childElement.setLevel(getLevel()+1); childElement.readBytes(reader); }else{ ResXmlStartElement startElement=new ResXmlStartElement(); setStartElement(startElement); startElement.readBytes(reader); } } private void onEndElement(BlockReader reader) throws IOException{ if(hasEndElement()){ multipleEndElement(reader); return; } ResXmlEndElement endElement=new ResXmlEndElement(); setEndElement(endElement); endElement.readBytes(reader); } private void onStartNamespace(BlockReader reader) throws IOException{ ResXmlStartNamespace startNamespace=new ResXmlStartNamespace(); addStartNamespace(startNamespace); startNamespace.readBytes(reader); } private void onEndNamespace(BlockReader reader) throws IOException{ ResXmlEndNamespace endNamespace=new ResXmlEndNamespace(); addEndNamespace(endNamespace); endNamespace.readBytes(reader); } private void onXmlText(BlockReader reader) throws IOException{ ResXmlText xmlText=new ResXmlText(); addResXmlText(xmlText); xmlText.readBytes(reader); } private void unknownChunk(BlockReader reader, HeaderBlock headerBlock) throws IOException{ throw new IOException("Unknown chunk: "+headerBlock.toString()); } private void multipleEndElement(BlockReader reader) throws IOException{ throw new IOException("Multiple end element: "+reader.toString()); } private void unexpectedChunk(BlockReader reader, HeaderBlock headerBlock) throws IOException{ throw new IOException("Unexpected chunk: "+headerBlock.toString()); } private void unBalancedFinish(BlockReader reader) throws IOException{ if(!isNamespaceBalanced()){ throw new IOException("Unbalanced namespace: start=" +mStartNamespaceList.size()+", end="+mEndNamespaceList.size()); } if(!isElementBalanced()){ // Should not happen unless corrupted file, auto corrected above StringBuilder builder=new StringBuilder(); builder.append("Unbalanced element: start="); ResXmlStartElement startElement=getStartElement(); if(startElement!=null){ builder.append(startElement); }else { builder.append("null"); } builder.append(", end="); ResXmlEndElement endElement=getEndElement(); if(endElement!=null){ builder.append(endElement); }else { builder.append("null"); } throw new IOException(builder.toString()); } } @Override public JSONObject toJson() { JSONObject jsonObject=new JSONObject(); jsonObject.put(NAME_node_type, NAME_element); ResXmlStartElement start = getStartElement(); jsonObject.put(NAME_line, start.getLineNumber()); int i=0; JSONArray nsList=new JSONArray(); for(ResXmlStartNamespace namespace:getStartNamespaceList()){ JSONObject ns=new JSONObject(); ns.put(NAME_namespace_uri, namespace.getUri()); ns.put(NAME_namespace_prefix, namespace.getPrefix()); nsList.put(i, ns); i++; } if(i>0){ jsonObject.put(NAME_namespaces, nsList); } jsonObject.put(NAME_name, start.getName()); String comment=start.getComment(); if(comment!=null){ jsonObject.put(NAME_comment, comment); } String uri=start.getUri(); if(uri!=null){ jsonObject.put(NAME_namespace_uri, uri); } JSONArray attrArray=start.getResXmlAttributeArray().toJson(); jsonObject.put(NAME_attributes, attrArray); i=0; JSONArray childes=new JSONArray(); for(ResXmlNode xmlNode: getXmlNodes()){ childes.put(i, xmlNode.toJson()); i++; } if(i>0){ jsonObject.put(NAME_childes, childes); } return jsonObject; } @Override public void fromJson(JSONObject json) { ResXmlStartElement start = getStartElement(); int lineNo=json.optInt(NAME_line, 1); if(start==null){ start = newStartElement(lineNo); }else { start.setLineNumber(lineNo); } JSONArray nsArray = json.optJSONArray(NAME_namespaces); if(nsArray!=null){ int length=nsArray.length(); for(int i=0;i"); builder.append(text.toString()); builder.append(""); }else { builder.append("/>"); } return builder.toString(); } return "NULL"; } static ResXmlElement newResXmlElement(String tag){ ResXmlElement resXmlElement=new ResXmlElement(); ResXmlStartElement startElement=new ResXmlStartElement(); resXmlElement.setStartElement(startElement); ResXmlEndElement endElement=new ResXmlEndElement(); resXmlElement.setEndElement(endElement); resXmlElement.setTag(tag); return resXmlElement; } public static final String NS_ANDROID_URI = "http://schemas.android.com/apk/res/android"; public static final String NS_ANDROID_PREFIX = "android"; static final String NAME_element = "element"; static final String NAME_name = "name"; static final String NAME_comment = "comment"; static final String NAME_text = "text"; static final String NAME_namespaces = "namespaces"; static final String NAME_namespace_uri = "namespace_uri"; static final String NAME_namespace_prefix = "namespace_prefix"; private static final String NAME_line = "line"; static final String NAME_attributes = "attributes"; static final String NAME_childes = "childes"; }