/ app / src / main / java / com / reandroid / arsc / item / StringItem.java
StringItem.java
  1  /*
  2    *  Copyright (C) 2022 github.com/REAndroid
  3    *
  4    *  Licensed under the Apache License, Version 2.0 (the "License");
  5    *  you may not use this file except in compliance with the License.
  6    *  You may obtain a copy of the License at
  7    *
  8    *      http://www.apache.org/licenses/LICENSE-2.0
  9    *
 10    * Unless required by applicable law or agreed to in writing, software
 11    * distributed under the License is distributed on an "AS IS" BASIS,
 12    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13    * See the License for the specific language governing permissions and
 14    * limitations under the License.
 15    */
 16  package com.reandroid.arsc.item;
 17  
 18  import com.reandroid.arsc.array.StringArray;
 19  import com.reandroid.arsc.base.Block;
 20  import com.reandroid.arsc.coder.ThreeByteCharsetDecoder;
 21  import com.reandroid.arsc.coder.XmlSanitizer;
 22  import com.reandroid.arsc.io.BlockReader;
 23  import com.reandroid.arsc.pool.StringPool;
 24  import com.reandroid.json.JSONConvert;
 25  import com.reandroid.json.JSONObject;
 26  import com.reandroid.utils.CompareUtil;
 27  import com.reandroid.utils.ObjectsUtil;
 28  import com.reandroid.utils.collection.ComputeIterator;
 29  import com.reandroid.utils.collection.EmptyIterator;
 30  import com.reandroid.utils.collection.FilterIterator;
 31  import com.reandroid.xml.StyleDocument;
 32  import org.xmlpull.v1.XmlSerializer;
 33  
 34  import java.io.IOException;
 35  import java.io.UnsupportedEncodingException;
 36  import java.nio.ByteBuffer;
 37  import java.nio.CharBuffer;
 38  import java.nio.charset.CharacterCodingException;
 39  import java.nio.charset.CharsetDecoder;
 40  import java.nio.charset.StandardCharsets;
 41  import java.util.Collection;
 42  import java.util.HashSet;
 43  import java.util.Iterator;
 44  import java.util.Set;
 45  import java.util.function.Predicate;
 46  
 47  public class StringItem extends StringBlock implements JSONConvert<JSONObject>, Comparable<StringItem> {
 48  
 49      private boolean mUtf8;
 50      private final Set<ReferenceItem> mReferencedList;
 51      private StyleItem mStyleItem;
 52  
 53      public StringItem(boolean utf8) {
 54          super();
 55          this.mUtf8 = utf8;
 56          this.mReferencedList = new HashSet<>();
 57      }
 58  
 59      public StyleDocument getStyleDocument() {
 60          if(hasStyle()){
 61              return getStyle().build(get());
 62          }
 63          return null;
 64      }
 65  
 66      public<T extends Block> Iterator<T> getUsers(Class<T> parentClass){
 67          return getUsers(parentClass, null);
 68      }
 69      public<T extends Block> Iterator<T> getUsers(Class<T> parentClass,
 70                                                   Predicate<T> resultFilter){
 71  
 72          Collection<ReferenceItem> referencedList = getReferencedList();
 73          if(referencedList.size() == 0){
 74              return EmptyIterator.of();
 75          }
 76          return new ComputeIterator<>(referencedList.iterator(), referenceItem -> {
 77              T result = referenceItem.getReferredParent(parentClass);
 78              if (result == null || resultFilter != null && !resultFilter.test(result)) {
 79                  result = null;
 80              }
 81              return result;
 82          });
 83  
 84      }
 85  
 86      public boolean removeReference(ReferenceItem ref){
 87          return mReferencedList.remove(ref);
 88      }
 89      public void removeAllReference(){
 90          mReferencedList.clear();
 91      }
 92      public boolean hasReference(){
 93          ensureStringLinkUnlocked();
 94          if(mReferencedList.size() == 0) {
 95              return false;
 96          }
 97          return FilterIterator.of(mReferencedList.iterator(),
 98                  referenceItem -> !(referenceItem instanceof StyleItem.StyleIndexReference))
 99                  .hasNext();
100      }
101      public Collection<ReferenceItem> getReferencedList(){
102          ensureStringLinkUnlocked();
103          return mReferencedList;
104      }
105      void ensureStringLinkUnlocked(){
106          StringPool<?> stringPool = getParentInstance(StringPool.class);
107          if(stringPool != null){
108              stringPool.ensureStringLinkUnlockedInternal();
109          }
110      }
111      public void addReference(ReferenceItem ref){
112          if(ref!=null){
113              mReferencedList.add(ref);
114          }
115      }
116      public void addReferenceIfAbsent(ReferenceItem ref){
117          if(ref!=null){
118              mReferencedList.add(ref);
119          }
120      }
121      public void addReference(Collection<ReferenceItem> refList){
122          if(refList == null){
123              return;
124          }
125          for(ReferenceItem ref:refList){
126              if(ref != null){
127                  this.mReferencedList.add(ref);
128              }
129          }
130      }
131      private void reUpdateReferences(int newIndex){
132          ReferenceItem[] referenceItems = mReferencedList.toArray(new ReferenceItem[0]);
133          for(ReferenceItem ref:referenceItems){
134              ref.set(newIndex);
135          }
136      }
137      public void onRemoved(){
138          clearStyle();
139          setParent(null);
140      }
141      @Override
142      public void onIndexChanged(int oldIndex, int newIndex){
143          reUpdateReferences(newIndex);
144      }
145      @SuppressWarnings("unchecked")
146      @Override
147      protected void onStringChanged(String old, String text) {
148          super.onStringChanged(old, text);
149          StringPool<StringItem> stringPool = getParentInstance(StringPool.class);
150          if(stringPool != null) {
151              stringPool.onStringChanged(old, this);
152          }
153      }
154  
155      public void serializeText(XmlSerializer serializer) throws IOException {
156          serializeText(serializer, false);
157      }
158      public void serializeText(XmlSerializer serializer, boolean escapeValues) throws IOException {
159          String text = get();
160          if(text == null){
161              return;
162          }
163          if(escapeValues){
164              text = XmlSanitizer.escapeDecodedValue(text);
165          }else {
166              text = XmlSanitizer.escapeSpecialCharacter(text);
167          }
168          serializer.text(text);
169      }
170      public void serializeAttribute(XmlSerializer serializer, String namespace, String name) throws IOException {
171          String text = get();
172          if(text == null){
173              // TODO: could happen?
174              text = "";
175          }
176          serializer.attribute(namespace, name, XmlSanitizer.escapeSpecialCharacter(text));
177      }
178      public String getHtml(){
179          String text = get();
180          if(text == null){
181              return null;
182          }
183          StyleItem styleItem = getStyle();
184          if(styleItem == null){
185              return text;
186          }
187          return styleItem.applyStyle(text, false, false);
188      }
189      public String getXml(){
190          return getXml(false);
191      }
192      public String getXml(boolean escapeXmlText){
193          String text = get();
194          if(text == null){
195              return null;
196          }
197          StyleItem styleItem = getStyle();
198          if(styleItem == null){
199              return text;
200          }
201          return styleItem.applyStyle(text, true, escapeXmlText);
202      }
203      @Override
204      public void set(String str){
205          if(str == null){
206              StyleItem style = getStyle();
207              if(style != null){
208                  style.clearStyle();
209              }
210          }
211          super.set(str);
212      }
213      public void set(StyleDocument document){
214          String old = getXml();
215          if(countBytes() == 0) {
216              old = null;
217          }
218          clearStyle();
219          this.set(document.getStyledString(), false);
220          if(document.hasElements()) {
221              StyleItem styleItem = getOrCreateStyle();
222              styleItem.parse(document);
223          }
224          String update = getXml();
225          onStringChanged(old, update);
226      }
227      public void set(JSONObject jsonObject){
228          String old = getXml();
229          if(countBytes() == 0) {
230              old = null;
231          }
232          clearStyle();
233          this.set(jsonObject.getString(NAME_string), false);
234          JSONObject style = jsonObject.optJSONObject(NAME_style);
235          if(style != null) {
236              StyleItem styleItem = getOrCreateStyle();
237              styleItem.fromJson(style);
238          }
239          String update = getXml();
240          onStringChanged(old, update);
241      }
242  
243      public boolean isUtf8(){
244          return mUtf8;
245      }
246      public void setUtf8(boolean utf8){
247          if(utf8 == mUtf8){
248              return;
249          }
250          mUtf8 = utf8;
251          onBytesChanged();
252      }
253      @Override
254      public void onReadBytes(BlockReader reader) throws IOException {
255          if(reader.available() < 4){
256              return;
257          }
258          setBytesLength(calculateReadLength(reader), false);
259          reader.readFully(getBytesInternal());
260          onBytesChanged();
261      }
262      int calculateReadLength(BlockReader reader) throws IOException {
263          if(reader.available() < 4){
264              return reader.available();
265          }
266          byte[] bytes = new byte[4];
267          reader.readFully(bytes);
268          reader.offset(-4);
269          int[] lengthResult;
270          if(isUtf8()){
271              lengthResult = decodeUtf8StringByteLength(bytes);
272          }else {
273              lengthResult = decodeUtf16StringByteLength(bytes);
274          }
275          int add = isUtf8()? 1:2;
276          return lengthResult[0] + lengthResult[1] + add;
277      }
278      @Override
279      protected String decodeString(byte[] bytes){
280          return decodeString(bytes, mUtf8);
281      }
282      @Override
283      protected byte[] encodeString(String str){
284          if(mUtf8){
285              return encodeUtf8ToBytes(str);
286          }else {
287              return encodeUtf16ToBytes(str);
288          }
289      }
290      private String decodeString(byte[] allStringBytes, boolean isUtf8) {
291          if(isNullBytes(allStringBytes)){
292              if(allStringBytes==null||allStringBytes.length==0){
293                  return null;
294              }
295              return "";
296          }
297          int[] offLen;
298          if(isUtf8){
299              offLen=decodeUtf8StringByteLength(allStringBytes);
300          }else {
301              offLen=decodeUtf16StringByteLength(allStringBytes);
302          }
303          CharsetDecoder charsetDecoder;
304          if(isUtf8){
305              charsetDecoder=UTF8_DECODER;
306          }else {
307              charsetDecoder=UTF16LE_DECODER;
308          }
309          try {
310              ByteBuffer buf=ByteBuffer.wrap(allStringBytes, offLen[0], offLen[1]);
311              CharBuffer charBuffer=charsetDecoder.decode(buf);
312              return charBuffer.toString();
313          } catch (CharacterCodingException ex) {
314              if(isUtf8){
315                  return tryThreeByteDecoder(allStringBytes, offLen[0], offLen[1]);
316              }
317              return new String(allStringBytes, offLen[0], offLen[1], StandardCharsets.UTF_16LE);
318          }
319      }
320      private String tryThreeByteDecoder(byte[] bytes, int offset, int length){
321          try {
322              ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, offset, length);
323              CharBuffer charBuffer = DECODER_3B.decode(byteBuffer);
324              return charBuffer.toString();
325          } catch (CharacterCodingException e) {
326              return new String(bytes, offset, length, StandardCharsets.UTF_8);
327          }
328      }
329      public boolean hasStyle(){
330          StyleItem styleItem=getStyle();
331          if(styleItem==null){
332              return false;
333          }
334          return styleItem.size()>0;
335      }
336      public StyleItem getStyle(){
337          return mStyleItem;
338      }
339      public StyleItem getOrCreateStyle(){
340          StyleItem styleItem = getStyle();
341          if(styleItem == null) {
342              styleItem = getParentInstance(StringPool.class).getStyleArray().createNext();
343              linkStyleItemInternal(styleItem);
344              styleItem = getStyle();
345          }
346          return styleItem;
347      }
348      public void linkStyleItemInternal(StyleItem styleItem) {
349          if(styleItem == null) {
350              throw new NullPointerException("Can not link null style item");
351          }
352          if(this.mStyleItem == styleItem) {
353              return;
354          }
355          if(this.mStyleItem != null) {
356              throw new IllegalStateException("Style item is already linked");
357          }
358          this.mStyleItem = styleItem;
359          styleItem.setStringItemInternal(this);
360      }
361      public void unlinkStyleItemInternal(StyleItem styleItem) {
362          if(this.mStyleItem == null) {
363              return;
364          }
365          if(styleItem != this.mStyleItem) {
366              throw new IllegalStateException("Wrong style item");
367          }
368          this.mStyleItem = null;
369          styleItem.setStringItemInternal(null);
370      }
371      private void clearStyle() {
372          StyleItem styleItem = getStyle();
373          if(styleItem != null) {
374              styleItem.clearStyle();
375          }
376      }
377      public void transferReferences(StringItem source){
378          if(source == this || source == null || getParent() != source.getParent()){
379              return;
380          }
381          int index = getIndex();
382          if(index < 0 || source.getIndex() < 0){
383              return;
384          }
385          ReferenceItem[] copyList = source.getReferencedList().toArray(new ReferenceItem[0]);
386          for(ReferenceItem ref : copyList){
387              if(isTransferable(ref)){
388                  source.removeReference(ref);
389                  ref.set(index);
390                  addReference(ref);
391              }
392          }
393      }
394      private boolean isTransferable(ReferenceItem referenceItem){
395          return !((referenceItem instanceof WeakStringReference));
396      }
397      public boolean merge(StringItem other) {
398          if(!canMerge(other)) {
399              return false;
400          }
401          clearStyle();
402          set(other.get(), false);
403          StyleItem otherStyle = other.getStyle();
404          if(otherStyle != null && otherStyle.hasSpans()) {
405              getOrCreateStyle().merge(otherStyle);
406          }
407          onStringChanged(null, getXml());
408          return true;
409      }
410      boolean canMerge(StringItem stringItem) {
411          if(stringItem == null || stringItem == this) {
412              return false;
413          }
414          Block array1 = this.getParentInstance(StringArray.class);
415          Block array2 = stringItem.getParentInstance(StringArray.class);
416          return array1 != null && array2 != null && array1 != array2;
417      }
418      @Override
419      public int compareTo(StringItem stringItem) {
420          if(stringItem == null){
421              return -1;
422          }
423          if(stringItem == this) {
424              return 0;
425          }
426          int i = -1 * CompareUtil.compare(hasStyle(), stringItem.hasStyle());
427          if(i != 0) {
428              return i;
429          }
430          return CompareUtil.compare(getXml(), stringItem.getXml());
431      }
432      @Override
433      public JSONObject toJson() {
434          JSONObject jsonObject = new JSONObject();
435          jsonObject.put(NAME_string, get());
436          if(hasStyle()) {
437              jsonObject.put(NAME_style, getStyle().toJson());
438          }
439          return jsonObject;
440      }
441      @Override
442      public void fromJson(JSONObject json) {
443          set(json);
444      }
445      @Override
446      public String toString(){
447          String xml = getXml();
448          if(xml == null){
449              return getIndex() + ": NULL";
450          }
451          StringPool<?> stringPool = getParentInstance(StringPool.class);
452          if(stringPool != null && !stringPool.isStringLinkLocked()){
453              return getIndex() + ": USED BY=" + mReferencedList.size() + "{" + xml + "}";
454          }
455          return getIndex() + ":" + xml;
456      }
457  
458      private static int[] decodeUtf8StringByteLength(byte[] lengthBytes) {
459          int offset=0;
460          int val = lengthBytes[offset];
461          int length;
462          if ((val & 0x80) != 0) {
463              offset += 2;
464          } else {
465              offset += 1;
466          }
467          val = lengthBytes[offset];
468          offset += 1;
469          if ((val & 0x80) != 0) {
470              int low = (lengthBytes[offset] & 0xFF);
471              length = val & 0x7F;
472              length = length << 8;
473              length = length + low;
474              offset += 1;
475          } else {
476              length = val;
477          }
478          return new int[] { offset, length};
479      }
480      private static int[] decodeUtf16StringByteLength(byte[] lengthBytes) {
481          int val = ((lengthBytes[1] & 0xFF) << 8 | lengthBytes[0] & 0xFF);
482          if ((val & 0x8000) != 0) {
483              int high = (lengthBytes[3] & 0xFF) << 8;
484              int low = (lengthBytes[2] & 0xFF);
485              int len_value =  ((val & 0x7FFF) << 16) + (high + low);
486              return new int[] {4, len_value * 2};
487  
488          }
489          return new int[] {2, val * 2};
490      }
491      static boolean isNullBytes(byte[] bts){
492          if(bts==null){
493              return true;
494          }
495          int max=bts.length;
496          if(max<2){
497              return true;
498          }
499          for(int i=2; i<max;i++){
500              if(bts[i] != 0){
501                  return false;
502              }
503          }
504          return true;
505      }
506  
507  
508      private static byte[] encodeUtf8ToBytes(String str){
509          byte[] bts;
510          byte[] lenBytes=new byte[2];
511          if(str!=null){
512              bts=str.getBytes();
513              int strLen=bts.length;
514              if((strLen & 0xff80)!=0){
515                  lenBytes=new byte[4];
516                  int l2=strLen&0xff;
517                  int l1=(strLen-l2)>>8;
518                  lenBytes[3]=(byte) (l2);
519                  lenBytes[2]=(byte) (l1|0x80);
520                  strLen=str.length();
521                  l2=strLen&0xff;
522                  l1=(strLen-l2)>>8;
523                  lenBytes[1]=(byte) (l2);
524                  lenBytes[0]=(byte) (l1|0x80);
525              }else{
526                  lenBytes=new ShortItem((short) strLen).getBytesInternal();
527                  lenBytes[1]=lenBytes[0];
528                  lenBytes[0]=(byte)str.length();
529              }
530          }else {
531              bts=new byte[0];
532          }
533          return addBytes(lenBytes, bts, new byte[1]);
534      }
535      private static byte[] encodeUtf16ToBytes(String str){
536          if(str==null){
537              return null;
538          }
539          byte[] lenBytes;
540          byte[] bts=getUtf16Bytes(str);
541          int strLen=bts.length;
542          strLen=strLen/2;
543          if((strLen & 0xffff8000)!=0){
544              lenBytes=new byte[4];
545              int low=strLen&0xff;
546              int high=(strLen-low)&0xff00;
547              int rem=strLen-low-high;
548              lenBytes[3]=(byte) (high>>8);
549              lenBytes[2]=(byte) (low);
550              low=rem&0xff;
551              high=(rem&0xff00)>>8;
552              lenBytes[1]=(byte) (high|0x80);
553              lenBytes[0]=(byte) (low);
554          }else{
555              lenBytes=new ShortItem((short) strLen).getBytesInternal();
556          }
557          return addBytes(lenBytes, bts, new byte[2]);
558      }
559      static byte[] getUtf16Bytes(String str){
560          try {
561              return str.getBytes("UTF-16LE");
562          } catch (UnsupportedEncodingException e) {
563              throw new RuntimeException(e);
564          }
565      }
566  
567      private static byte[] addBytes(byte[] bts1, byte[] bts2, byte[] bts3){
568          if(bts1==null && bts2==null && bts3==null){
569              return null;
570          }
571          int len=0;
572          if(bts1!=null){
573              len=bts1.length;
574          }
575          if(bts2!=null){
576              len+=bts2.length;
577          }
578          if(bts3!=null){
579              len+=bts3.length;
580          }
581          byte[] result=new byte[len];
582          int start=0;
583          if(bts1!=null){
584              start=bts1.length;
585              System.arraycopy(bts1, 0, result, 0, start);
586          }
587          if(bts2!=null){
588              System.arraycopy(bts2, 0, result, start, bts2.length);
589              start+=bts2.length;
590          }
591          if(bts3!=null){
592              System.arraycopy(bts3, 0, result, start, bts3.length);
593          }
594          return result;
595      }
596  
597      private static final CharsetDecoder UTF16LE_DECODER = StandardCharsets.UTF_16LE.newDecoder();
598      private static final CharsetDecoder DECODER_3B = ThreeByteCharsetDecoder.INSTANCE;
599  
600      public static final String NAME_string = ObjectsUtil.of("string");
601      public static final String NAME_style = ObjectsUtil.of("style");
602  }