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 }