Source.java revision 1204:9597425b6b38
1/* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.nashorn.internal.runtime; 27 28import java.io.ByteArrayOutputStream; 29import java.io.File; 30import java.io.FileNotFoundException; 31import java.io.IOError; 32import java.io.IOException; 33import java.io.InputStream; 34import java.io.Reader; 35import java.lang.ref.WeakReference; 36import java.net.MalformedURLException; 37import java.net.URISyntaxException; 38import java.net.URL; 39import java.net.URLConnection; 40import java.nio.charset.Charset; 41import java.nio.charset.StandardCharsets; 42import java.nio.file.Files; 43import java.nio.file.Path; 44import java.nio.file.Paths; 45import java.security.MessageDigest; 46import java.security.NoSuchAlgorithmException; 47import java.util.Arrays; 48import java.util.Base64; 49import java.util.Objects; 50import java.util.WeakHashMap; 51import jdk.nashorn.api.scripting.URLReader; 52import jdk.nashorn.internal.parser.Token; 53import jdk.nashorn.internal.runtime.logging.DebugLogger; 54import jdk.nashorn.internal.runtime.logging.Loggable; 55import jdk.nashorn.internal.runtime.logging.Logger; 56/** 57 * Source objects track the origin of JavaScript entities. 58 */ 59@Logger(name="source") 60public final class Source implements Loggable { 61 private static final int BUF_SIZE = 8 * 1024; 62 private static final Cache CACHE = new Cache(); 63 64 // Message digest to file name encoder 65 private final static Base64.Encoder BASE64 = Base64.getUrlEncoder().withoutPadding(); 66 67 /** 68 * Descriptive name of the source as supplied by the user. Used for error 69 * reporting to the user. For example, SyntaxError will use this to print message. 70 * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage. 71 */ 72 private final String name; 73 74 /** 75 * Base directory the File or base part of the URL. Used to implement __DIR__. 76 * Used to load scripts relative to the 'directory' or 'base' URL of current script. 77 * This will be null when it can't be computed. 78 */ 79 private final String base; 80 81 /** Source content */ 82 private final Data data; 83 84 /** Cached hash code */ 85 private int hash; 86 87 /** Base64-encoded SHA1 digest of this source object */ 88 private volatile byte[] digest; 89 90 /** source URL set via //@ sourceURL or //# sourceURL directive */ 91 private String explicitURL; 92 93 // Do *not* make this public, ever! Trusts the URL and content. 94 private Source(final String name, final String base, final Data data) { 95 this.name = name; 96 this.base = base; 97 this.data = data; 98 } 99 100 private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException { 101 try { 102 final Source newSource = new Source(name, base, data); 103 final Source existingSource = CACHE.get(newSource); 104 if (existingSource != null) { 105 // Force any access errors 106 data.checkPermissionAndClose(); 107 return existingSource; 108 } 109 110 // All sources in cache must be fully loaded 111 data.load(); 112 CACHE.put(newSource, newSource); 113 114 return newSource; 115 } catch (final RuntimeException e) { 116 final Throwable cause = e.getCause(); 117 if (cause instanceof IOException) { 118 throw (IOException) cause; 119 } 120 throw e; 121 } 122 } 123 124 private static class Cache extends WeakHashMap<Source, WeakReference<Source>> { 125 public Source get(final Source key) { 126 final WeakReference<Source> ref = super.get(key); 127 return ref == null ? null : ref.get(); 128 } 129 130 public void put(final Source key, final Source value) { 131 assert !(value.data instanceof RawData); 132 put(key, new WeakReference<>(value)); 133 } 134 } 135 136 /* package-private */ 137 DebuggerSupport.SourceInfo getSourceInfo() { 138 return new DebuggerSupport.SourceInfo(getName(), data.hashCode(), data.url(), data.array()); 139 } 140 141 // Wrapper to manage lazy loading 142 private static interface Data { 143 144 URL url(); 145 146 int length(); 147 148 long lastModified(); 149 150 char[] array(); 151 152 boolean isEvalCode(); 153 } 154 155 private static class RawData implements Data { 156 private final char[] array; 157 private final boolean evalCode; 158 private int hash; 159 160 private RawData(final char[] array, final boolean evalCode) { 161 this.array = Objects.requireNonNull(array); 162 this.evalCode = evalCode; 163 } 164 165 private RawData(final String source, final boolean evalCode) { 166 this.array = Objects.requireNonNull(source).toCharArray(); 167 this.evalCode = evalCode; 168 } 169 170 private RawData(final Reader reader) throws IOException { 171 this(readFully(reader), false); 172 } 173 174 @Override 175 public int hashCode() { 176 int h = hash; 177 if (h == 0) { 178 h = hash = Arrays.hashCode(array) ^ (evalCode? 1 : 0); 179 } 180 return h; 181 } 182 183 @Override 184 public boolean equals(final Object obj) { 185 if (this == obj) { 186 return true; 187 } 188 if (obj instanceof RawData) { 189 final RawData other = (RawData)obj; 190 return Arrays.equals(array, other.array) && evalCode == other.evalCode; 191 } 192 return false; 193 } 194 195 @Override 196 public String toString() { 197 return new String(array()); 198 } 199 200 @Override 201 public URL url() { 202 return null; 203 } 204 205 @Override 206 public int length() { 207 return array.length; 208 } 209 210 @Override 211 public long lastModified() { 212 return 0; 213 } 214 215 @Override 216 public char[] array() { 217 return array; 218 } 219 220 221 @Override 222 public boolean isEvalCode() { 223 return evalCode; 224 } 225 } 226 227 private static class URLData implements Data { 228 private final URL url; 229 protected final Charset cs; 230 private int hash; 231 protected char[] array; 232 protected int length; 233 protected long lastModified; 234 235 private URLData(final URL url, final Charset cs) { 236 this.url = Objects.requireNonNull(url); 237 this.cs = cs; 238 } 239 240 @Override 241 public int hashCode() { 242 int h = hash; 243 if (h == 0) { 244 h = hash = url.hashCode(); 245 } 246 return h; 247 } 248 249 @Override 250 public boolean equals(final Object other) { 251 if (this == other) { 252 return true; 253 } 254 if (!(other instanceof URLData)) { 255 return false; 256 } 257 258 final URLData otherData = (URLData) other; 259 260 if (url.equals(otherData.url)) { 261 // Make sure both have meta data loaded 262 try { 263 if (isDeferred()) { 264 // Data in cache is always loaded, and we only compare to cached data. 265 assert !otherData.isDeferred(); 266 loadMeta(); 267 } else if (otherData.isDeferred()) { 268 otherData.loadMeta(); 269 } 270 } catch (final IOException e) { 271 throw new RuntimeException(e); 272 } 273 274 // Compare meta data 275 return this.length == otherData.length && this.lastModified == otherData.lastModified; 276 } 277 return false; 278 } 279 280 @Override 281 public String toString() { 282 return new String(array()); 283 } 284 285 @Override 286 public URL url() { 287 return url; 288 } 289 290 @Override 291 public int length() { 292 return length; 293 } 294 295 @Override 296 public long lastModified() { 297 return lastModified; 298 } 299 300 @Override 301 public char[] array() { 302 assert !isDeferred(); 303 return array; 304 } 305 306 @Override 307 public boolean isEvalCode() { 308 return false; 309 } 310 311 boolean isDeferred() { 312 return array == null; 313 } 314 315 @SuppressWarnings("try") 316 protected void checkPermissionAndClose() throws IOException { 317 try (InputStream in = url.openStream()) { 318 // empty 319 } 320 debug("permission checked for ", url); 321 } 322 323 protected void load() throws IOException { 324 if (array == null) { 325 final URLConnection c = url.openConnection(); 326 try (InputStream in = c.getInputStream()) { 327 array = cs == null ? readFully(in) : readFully(in, cs); 328 length = array.length; 329 lastModified = c.getLastModified(); 330 debug("loaded content for ", url); 331 } 332 } 333 } 334 335 protected void loadMeta() throws IOException { 336 if (length == 0 && lastModified == 0) { 337 final URLConnection c = url.openConnection(); 338 length = c.getContentLength(); 339 lastModified = c.getLastModified(); 340 debug("loaded metadata for ", url); 341 } 342 } 343 } 344 345 private static class FileData extends URLData { 346 private final File file; 347 348 private FileData(final File file, final Charset cs) { 349 super(getURLFromFile(file), cs); 350 this.file = file; 351 352 } 353 354 @Override 355 protected void checkPermissionAndClose() throws IOException { 356 if (!file.canRead()) { 357 throw new FileNotFoundException(file + " (Permission Denied)"); 358 } 359 debug("permission checked for ", file); 360 } 361 362 @Override 363 protected void loadMeta() { 364 if (length == 0 && lastModified == 0) { 365 length = (int) file.length(); 366 lastModified = file.lastModified(); 367 debug("loaded metadata for ", file); 368 } 369 } 370 371 @Override 372 protected void load() throws IOException { 373 if (array == null) { 374 array = cs == null ? readFully(file) : readFully(file, cs); 375 length = array.length; 376 lastModified = file.lastModified(); 377 debug("loaded content for ", file); 378 } 379 } 380 } 381 382 private static void debug(final Object... msg) { 383 final DebugLogger logger = getLoggerStatic(); 384 if (logger != null) { 385 logger.info(msg); 386 } 387 } 388 389 private char[] data() { 390 return data.array(); 391 } 392 393 /** 394 * Returns a Source instance 395 * 396 * @param name source name 397 * @param content contents as char array 398 * @param isEval does this represent code from 'eval' call? 399 * @return source instance 400 */ 401 public static Source sourceFor(final String name, final char[] content, final boolean isEval) { 402 return new Source(name, baseName(name), new RawData(content, isEval)); 403 } 404 405 /** 406 * Returns a Source instance 407 * 408 * @param name source name 409 * @param content contents as char array 410 * 411 * @return source instance 412 */ 413 public static Source sourceFor(final String name, final char[] content) { 414 return sourceFor(name, content, false); 415 } 416 417 /** 418 * Returns a Source instance 419 * 420 * @param name source name 421 * @param content contents as string 422 * @param isEval does this represent code from 'eval' call? 423 * @return source instance 424 */ 425 public static Source sourceFor(final String name, final String content, final boolean isEval) { 426 return new Source(name, baseName(name), new RawData(content, isEval)); 427 } 428 429 /** 430 * Returns a Source instance 431 * 432 * @param name source name 433 * @param content contents as string 434 * @return source instance 435 */ 436 public static Source sourceFor(final String name, final String content) { 437 return sourceFor(name, content, false); 438 } 439 440 /** 441 * Constructor 442 * 443 * @param name source name 444 * @param url url from which source can be loaded 445 * 446 * @return source instance 447 * 448 * @throws IOException if source cannot be loaded 449 */ 450 public static Source sourceFor(final String name, final URL url) throws IOException { 451 return sourceFor(name, url, null); 452 } 453 454 /** 455 * Constructor 456 * 457 * @param name source name 458 * @param url url from which source can be loaded 459 * @param cs Charset used to convert bytes to chars 460 * 461 * @return source instance 462 * 463 * @throws IOException if source cannot be loaded 464 */ 465 public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException { 466 return sourceFor(name, baseURL(url), new URLData(url, cs)); 467 } 468 469 /** 470 * Constructor 471 * 472 * @param name source name 473 * @param file file from which source can be loaded 474 * 475 * @return source instance 476 * 477 * @throws IOException if source cannot be loaded 478 */ 479 public static Source sourceFor(final String name, final File file) throws IOException { 480 return sourceFor(name, file, null); 481 } 482 483 /** 484 * Constructor 485 * 486 * @param name source name 487 * @param path path from which source can be loaded 488 * 489 * @return source instance 490 * 491 * @throws IOException if source cannot be loaded 492 */ 493 public static Source sourceFor(final String name, final Path path) throws IOException { 494 File file = null; 495 try { 496 file = path.toFile(); 497 } catch (final UnsupportedOperationException uoe) { 498 } 499 500 if (file != null) { 501 return sourceFor(name, file); 502 } else { 503 return sourceFor(name, Files.newBufferedReader(path)); 504 } 505 } 506 507 /** 508 * Constructor 509 * 510 * @param name source name 511 * @param file file from which source can be loaded 512 * @param cs Charset used to convert bytes to chars 513 * 514 * @return source instance 515 * 516 * @throws IOException if source cannot be loaded 517 */ 518 public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException { 519 final File absFile = file.getAbsoluteFile(); 520 return sourceFor(name, dirName(absFile, null), new FileData(file, cs)); 521 } 522 523 /** 524 * Returns an instance 525 * 526 * @param name source name 527 * @param reader reader from which source can be loaded 528 * 529 * @return source instance 530 * 531 * @throws IOException if source cannot be loaded 532 */ 533 public static Source sourceFor(final String name, final Reader reader) throws IOException { 534 // Extract URL from URLReader to defer loading and reuse cached data if available. 535 if (reader instanceof URLReader) { 536 final URLReader urlReader = (URLReader) reader; 537 return sourceFor(name, urlReader.getURL(), urlReader.getCharset()); 538 } 539 return new Source(name, baseName(name), new RawData(reader)); 540 } 541 542 @Override 543 public boolean equals(final Object obj) { 544 if (this == obj) { 545 return true; 546 } 547 if (!(obj instanceof Source)) { 548 return false; 549 } 550 final Source other = (Source) obj; 551 return Objects.equals(name, other.name) && data.equals(other.data); 552 } 553 554 @Override 555 public int hashCode() { 556 int h = hash; 557 if (h == 0) { 558 h = hash = data.hashCode() ^ Objects.hashCode(name); 559 } 560 return h; 561 } 562 563 /** 564 * Fetch source content. 565 * @return Source content. 566 */ 567 public String getString() { 568 return data.toString(); 569 } 570 571 /** 572 * Get the user supplied name of this script. 573 * @return User supplied source name. 574 */ 575 public String getName() { 576 return name; 577 } 578 579 /** 580 * Get the last modified time of this script. 581 * @return Last modified time. 582 */ 583 public long getLastModified() { 584 return data.lastModified(); 585 } 586 587 /** 588 * Get the "directory" part of the file or "base" of the URL. 589 * @return base of file or URL. 590 */ 591 public String getBase() { 592 return base; 593 } 594 595 /** 596 * Fetch a portion of source content. 597 * @param start start index in source 598 * @param len length of portion 599 * @return Source content portion. 600 */ 601 public String getString(final int start, final int len) { 602 return new String(data(), start, len); 603 } 604 605 /** 606 * Fetch a portion of source content associated with a token. 607 * @param token Token descriptor. 608 * @return Source content portion. 609 */ 610 public String getString(final long token) { 611 final int start = Token.descPosition(token); 612 final int len = Token.descLength(token); 613 return new String(data(), start, len); 614 } 615 616 /** 617 * Returns the source URL of this script Source. Can be null if Source 618 * was created from a String or a char[]. 619 * 620 * @return URL source or null 621 */ 622 public URL getURL() { 623 return data.url(); 624 } 625 626 /** 627 * Get explicit source URL. 628 * @return URL set via sourceURL directive 629 */ 630 public String getExplicitURL() { 631 return explicitURL; 632 } 633 634 /** 635 * Set explicit source URL. 636 * @param explicitURL URL set via sourceURL directive 637 */ 638 public void setExplicitURL(final String explicitURL) { 639 this.explicitURL = explicitURL; 640 } 641 642 /** 643 * Returns whether this source was submitted via 'eval' call or not. 644 * 645 * @return true if this source represents code submitted via 'eval' 646 */ 647 public boolean isEvalCode() { 648 return data.isEvalCode(); 649 } 650 651 /** 652 * Find the beginning of the line containing position. 653 * @param position Index to offending token. 654 * @return Index of first character of line. 655 */ 656 private int findBOLN(final int position) { 657 final char[] d = data(); 658 for (int i = position - 1; i > 0; i--) { 659 final char ch = d[i]; 660 661 if (ch == '\n' || ch == '\r') { 662 return i + 1; 663 } 664 } 665 666 return 0; 667 } 668 669 /** 670 * Find the end of the line containing position. 671 * @param position Index to offending token. 672 * @return Index of last character of line. 673 */ 674 private int findEOLN(final int position) { 675 final char[] d = data(); 676 final int length = d.length; 677 for (int i = position; i < length; i++) { 678 final char ch = d[i]; 679 680 if (ch == '\n' || ch == '\r') { 681 return i - 1; 682 } 683 } 684 685 return length - 1; 686 } 687 688 /** 689 * Return line number of character position. 690 * 691 * <p>This method can be expensive for large sources as it iterates through 692 * all characters up to {@code position}.</p> 693 * 694 * @param position Position of character in source content. 695 * @return Line number. 696 */ 697 public int getLine(final int position) { 698 final char[] d = data(); 699 // Line count starts at 1. 700 int line = 1; 701 702 for (int i = 0; i < position; i++) { 703 final char ch = d[i]; 704 // Works for both \n and \r\n. 705 if (ch == '\n') { 706 line++; 707 } 708 } 709 710 return line; 711 } 712 713 /** 714 * Return column number of character position. 715 * @param position Position of character in source content. 716 * @return Column number. 717 */ 718 public int getColumn(final int position) { 719 // TODO - column needs to account for tabs. 720 return position - findBOLN(position); 721 } 722 723 /** 724 * Return line text including character position. 725 * @param position Position of character in source content. 726 * @return Line text. 727 */ 728 public String getSourceLine(final int position) { 729 // Find end of previous line. 730 final int first = findBOLN(position); 731 // Find end of this line. 732 final int last = findEOLN(position); 733 734 return new String(data(), first, last - first + 1); 735 } 736 737 /** 738 * Get the content of this source as a char array. Note that the underlying array is returned instead of a 739 * clone; modifying the char array will cause modification to the source; this should not be done. While 740 * there is an apparent danger that we allow unfettered access to an underlying mutable array, the 741 * {@code Source} class is in a restricted {@code jdk.nashorn.internal.*} package and as such it is 742 * inaccessible by external actors in an environment with a security manager. Returning a clone would be 743 * detrimental to performance. 744 * @return content the content of this source as a char array 745 */ 746 public char[] getContent() { 747 return data(); 748 } 749 750 /** 751 * Get the length in chars for this source 752 * @return length 753 */ 754 public int getLength() { 755 return data.length(); 756 } 757 758 /** 759 * Read all of the source until end of file. Return it as char array 760 * 761 * @param reader reader opened to source stream 762 * @return source as content 763 * @throws IOException if source could not be read 764 */ 765 public static char[] readFully(final Reader reader) throws IOException { 766 final char[] arr = new char[BUF_SIZE]; 767 final StringBuilder sb = new StringBuilder(); 768 769 try { 770 int numChars; 771 while ((numChars = reader.read(arr, 0, arr.length)) > 0) { 772 sb.append(arr, 0, numChars); 773 } 774 } finally { 775 reader.close(); 776 } 777 778 return sb.toString().toCharArray(); 779 } 780 781 /** 782 * Read all of the source until end of file. Return it as char array 783 * 784 * @param file source file 785 * @return source as content 786 * @throws IOException if source could not be read 787 */ 788 public static char[] readFully(final File file) throws IOException { 789 if (!file.isFile()) { 790 throw new IOException(file + " is not a file"); //TODO localize? 791 } 792 return byteToCharArray(Files.readAllBytes(file.toPath())); 793 } 794 795 /** 796 * Read all of the source until end of file. Return it as char array 797 * 798 * @param file source file 799 * @param cs Charset used to convert bytes to chars 800 * @return source as content 801 * @throws IOException if source could not be read 802 */ 803 public static char[] readFully(final File file, final Charset cs) throws IOException { 804 if (!file.isFile()) { 805 throw new IOException(file + " is not a file"); //TODO localize? 806 } 807 808 final byte[] buf = Files.readAllBytes(file.toPath()); 809 return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf); 810 } 811 812 /** 813 * Read all of the source until end of stream from the given URL. Return it as char array 814 * 815 * @param url URL to read content from 816 * @return source as content 817 * @throws IOException if source could not be read 818 */ 819 public static char[] readFully(final URL url) throws IOException { 820 return readFully(url.openStream()); 821 } 822 823 /** 824 * Read all of the source until end of file. Return it as char array 825 * 826 * @param url URL to read content from 827 * @param cs Charset used to convert bytes to chars 828 * @return source as content 829 * @throws IOException if source could not be read 830 */ 831 public static char[] readFully(final URL url, final Charset cs) throws IOException { 832 return readFully(url.openStream(), cs); 833 } 834 835 /** 836 * Get a Base64-encoded SHA1 digest for this source. 837 * 838 * @return a Base64-encoded SHA1 digest for this source 839 */ 840 public String getDigest() { 841 return new String(getDigestBytes(), StandardCharsets.US_ASCII); 842 } 843 844 private byte[] getDigestBytes() { 845 byte[] ldigest = digest; 846 if (ldigest == null) { 847 final char[] content = data(); 848 final byte[] bytes = new byte[content.length * 2]; 849 850 for (int i = 0; i < content.length; i++) { 851 bytes[i * 2] = (byte) (content[i] & 0x00ff); 852 bytes[i * 2 + 1] = (byte) ((content[i] & 0xff00) >> 8); 853 } 854 855 try { 856 final MessageDigest md = MessageDigest.getInstance("SHA-1"); 857 if (name != null) { 858 md.update(name.getBytes(StandardCharsets.UTF_8)); 859 } 860 if (base != null) { 861 md.update(base.getBytes(StandardCharsets.UTF_8)); 862 } 863 if (getURL() != null) { 864 md.update(getURL().toString().getBytes(StandardCharsets.UTF_8)); 865 } 866 digest = ldigest = BASE64.encode(md.digest(bytes)); 867 } catch (final NoSuchAlgorithmException e) { 868 throw new RuntimeException(e); 869 } 870 } 871 return ldigest; 872 } 873 874 /** 875 * Get the base url. This is currently used for testing only 876 * @param url a URL 877 * @return base URL for url 878 */ 879 public static String baseURL(final URL url) { 880 if (url.getProtocol().equals("file")) { 881 try { 882 final Path path = Paths.get(url.toURI()); 883 final Path parent = path.getParent(); 884 return (parent != null) ? (parent + File.separator) : null; 885 } catch (final SecurityException | URISyntaxException | IOError e) { 886 return null; 887 } 888 } 889 890 // FIXME: is there a better way to find 'base' URL of a given URL? 891 String path = url.getPath(); 892 if (path.isEmpty()) { 893 return null; 894 } 895 path = path.substring(0, path.lastIndexOf('/') + 1); 896 final int port = url.getPort(); 897 try { 898 return new URL(url.getProtocol(), url.getHost(), port, path).toString(); 899 } catch (final MalformedURLException e) { 900 return null; 901 } 902 } 903 904 private static String dirName(final File file, final String DEFAULT_BASE_NAME) { 905 final String res = file.getParent(); 906 return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME; 907 } 908 909 // fake directory like name 910 private static String baseName(final String name) { 911 int idx = name.lastIndexOf('/'); 912 if (idx == -1) { 913 idx = name.lastIndexOf('\\'); 914 } 915 return (idx != -1) ? name.substring(0, idx + 1) : null; 916 } 917 918 private static char[] readFully(final InputStream is, final Charset cs) throws IOException { 919 return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is); 920 } 921 922 private static char[] readFully(final InputStream is) throws IOException { 923 return byteToCharArray(readBytes(is)); 924 } 925 926 private static char[] byteToCharArray(final byte[] bytes) { 927 Charset cs = StandardCharsets.UTF_8; 928 int start = 0; 929 // BOM detection. 930 if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) { 931 start = 2; 932 cs = StandardCharsets.UTF_16BE; 933 } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) { 934 start = 2; 935 cs = StandardCharsets.UTF_16LE; 936 } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) { 937 start = 3; 938 cs = StandardCharsets.UTF_8; 939 } else if (bytes.length > 3 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE && bytes[2] == 0 && bytes[3] == 0) { 940 start = 4; 941 cs = Charset.forName("UTF-32LE"); 942 } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) { 943 start = 4; 944 cs = Charset.forName("UTF-32BE"); 945 } 946 947 return new String(bytes, start, bytes.length - start, cs).toCharArray(); 948 } 949 950 static byte[] readBytes(final InputStream is) throws IOException { 951 final byte[] arr = new byte[BUF_SIZE]; 952 try { 953 try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { 954 int numBytes; 955 while ((numBytes = is.read(arr, 0, arr.length)) > 0) { 956 buf.write(arr, 0, numBytes); 957 } 958 return buf.toByteArray(); 959 } 960 } finally { 961 is.close(); 962 } 963 } 964 965 @Override 966 public String toString() { 967 return getName(); 968 } 969 970 private static URL getURLFromFile(final File file) { 971 try { 972 return file.toURI().toURL(); 973 } catch (final SecurityException | MalformedURLException ignored) { 974 return null; 975 } 976 } 977 978 private static DebugLogger getLoggerStatic() { 979 final Context context = Context.getContextTrustedOrNull(); 980 return context == null ? null : context.getLogger(Source.class); 981 } 982 983 @Override 984 public DebugLogger initLogger(final Context context) { 985 return context.getLogger(this.getClass()); 986 } 987 988 @Override 989 public DebugLogger getLogger() { 990 return initLogger(Context.getContextTrusted()); 991 } 992} 993