View Javadoc
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  
26  package jdk.nashorn.internal.runtime;
27  
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.IOError;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.Reader;
34  import java.net.MalformedURLException;
35  import java.net.URISyntaxException;
36  import java.net.URL;
37  import java.nio.charset.Charset;
38  import java.nio.charset.StandardCharsets;
39  import java.nio.file.Files;
40  import java.nio.file.Path;
41  import java.nio.file.Paths;
42  import java.util.Arrays;
43  import java.util.Objects;
44  import jdk.nashorn.internal.parser.Token;
45  
46  /**
47   * Source objects track the origin of JavaScript entities.
48   *
49   */
50  public final class Source {
51      /**
52       * Descriptive name of the source as supplied by the user. Used for error
53       * reporting to the user. For example, SyntaxError will use this to print message.
54       * Used to implement __FILE__. Also used for SourceFile in .class for debugger usage.
55       */
56      private final String name;
57  
58      /**
59       * Base directory the File or base part of the URL. Used to implement __DIR__.
60       * Used to load scripts relative to the 'directory' or 'base' URL of current script.
61       * This will be null when it can't be computed.
62       */
63      private final String base;
64  
65      /** Cached source content. */
66      private final char[] content;
67  
68      /** Length of source content. */
69      private final int length;
70  
71      /** Cached hash code */
72      private int hash;
73  
74      /** Source URL if available */
75      private final URL url;
76  
77      private static final int BUFSIZE = 8 * 1024;
78  
79      // Do *not* make this public ever! Trusts the URL and content. So has to be called
80      // from other public constructors. Note that this can not be some init method as
81      // we initialize final fields from here.
82      private Source(final String name, final String base, final char[] content, final URL url) {
83          this.name    = name;
84          this.base    = base;
85          this.content = content;
86          this.length  = content.length;
87          this.url     = url;
88      }
89  
90      /**
91       * Constructor
92       *
93       * @param name    source name
94       * @param content contents as char array
95       */
96      public Source(final String name, final char[] content) {
97          this(name, baseName(name, null), content, null);
98      }
99  
100     /**
101      * Constructor
102      *
103      * @param name    source name
104      * @param content contents as string
105      */
106     public Source(final String name, final String content) {
107         this(name, content.toCharArray());
108     }
109 
110     /**
111      * Constructor
112      *
113      * @param name  source name
114      * @param url   url from which source can be loaded
115      *
116      * @throws IOException if source cannot be loaded
117      */
118     public Source(final String name, final URL url) throws IOException {
119         this(name, baseURL(url, null), readFully(url), url);
120     }
121 
122     /**
123      * Constructor
124      *
125      * @param name  source name
126      * @param url   url from which source can be loaded
127      * @param cs    Charset used to convert bytes to chars
128      *
129      * @throws IOException if source cannot be loaded
130      */
131     public Source(final String name, final URL url, final Charset cs) throws IOException {
132         this(name, baseURL(url, null), readFully(url, cs), url);
133     }
134 
135     /**
136      * Constructor
137      *
138      * @param name  source name
139      * @param file  file from which source can be loaded
140      *
141      * @throws IOException if source cannot be loaded
142      */
143     public Source(final String name, final File file) throws IOException {
144         this(name, dirName(file, null), readFully(file), getURLFromFile(file));
145     }
146 
147     /**
148      * Constructor
149      *
150      * @param name  source name
151      * @param file  file from which source can be loaded
152      * @param cs    Charset used to convert bytes to chars
153      *
154      * @throws IOException if source cannot be loaded
155      */
156     public Source(final String name, final File file, final Charset cs) throws IOException {
157         this(name, dirName(file, null), readFully(file, cs), getURLFromFile(file));
158     }
159 
160     @Override
161     public boolean equals(final Object obj) {
162         if (this == obj) {
163             return true;
164         }
165 
166         if (!(obj instanceof Source)) {
167             return false;
168         }
169 
170         final Source src = (Source)obj;
171         // Only compare content as a last resort measure
172         return length == src.length && Objects.equals(url, src.url) && Objects.equals(name, src.name) && Arrays.equals(content, src.content);
173     }
174 
175     @Override
176     public int hashCode() {
177         int h = hash;
178         if (h == 0) {
179             h = hash = Arrays.hashCode(content) ^ Objects.hashCode(name);
180         }
181         return h;
182     }
183 
184     /**
185      * Fetch source content.
186      * @return Source content.
187      */
188     public String getString() {
189         return new String(content, 0, length);
190     }
191 
192     /**
193      * Get the user supplied name of this script.
194      * @return User supplied source name.
195      */
196     public String getName() {
197         return name;
198     }
199 
200     /**
201      * Get the "directory" part of the file or "base" of the URL.
202      * @return base of file or URL.
203      */
204     public String getBase() {
205         return base;
206     }
207 
208     /**
209      * Fetch a portion of source content.
210      * @param start start index in source
211      * @param len length of portion
212      * @return Source content portion.
213      */
214     public String getString(final int start, final int len) {
215         return new String(content, start, len);
216     }
217 
218     /**
219      * Fetch a portion of source content associated with a token.
220      * @param token Token descriptor.
221      * @return Source content portion.
222      */
223     public String getString(final long token) {
224         final int start = Token.descPosition(token);
225         final int len = Token.descLength(token);
226         return new String(content, start, len);
227     }
228 
229     /**
230      * Returns the source URL of this script Source. Can be null if Source
231      * was created from a String or a char[].
232      *
233      * @return URL source or null
234      */
235     public URL getURL() {
236         return url;
237     }
238 
239     /**
240      * Find the beginning of the line containing position.
241      * @param position Index to offending token.
242      * @return Index of first character of line.
243      */
244     private int findBOLN(final int position) {
245         for (int i = position - 1; i > 0; i--) {
246             final char ch = content[i];
247 
248             if (ch == '\n' || ch == '\r') {
249                 return i + 1;
250             }
251         }
252 
253         return 0;
254     }
255 
256     /**
257      * Find the end of the line containing position.
258      * @param position Index to offending token.
259      * @return Index of last character of line.
260      */
261     private int findEOLN(final int position) {
262          for (int i = position; i < length; i++) {
263             final char ch = content[i];
264 
265             if (ch == '\n' || ch == '\r') {
266                 return i - 1;
267             }
268         }
269 
270         return length - 1;
271     }
272 
273     /**
274      * Return line number of character position.
275      *
276      * <p>This method can be expensive for large sources as it iterates through
277      * all characters up to {@code position}.</p>
278      *
279      * @param position Position of character in source content.
280      * @return Line number.
281      */
282     public int getLine(final int position) {
283         // Line count starts at 1.
284         int line = 1;
285 
286         for (int i = 0; i < position; i++) {
287             final char ch = content[i];
288             // Works for both \n and \r\n.
289             if (ch == '\n') {
290                 line++;
291             }
292         }
293 
294         return line;
295     }
296 
297     /**
298      * Return column number of character position.
299      * @param position Position of character in source content.
300      * @return Column number.
301      */
302     public int getColumn(final int position) {
303         // TODO - column needs to account for tabs.
304         return position - findBOLN(position);
305     }
306 
307     /**
308      * Return line text including character position.
309      * @param position Position of character in source content.
310      * @return Line text.
311      */
312     public String getSourceLine(final int position) {
313         // Find end of previous line.
314         final int first = findBOLN(position);
315         // Find end of this line.
316         final int last = findEOLN(position);
317 
318         return new String(content, first, last - first + 1);
319     }
320 
321     /**
322      * Get the content of this source as a char array
323      * @return content
324      */
325     public char[] getContent() {
326         return content.clone();
327     }
328 
329     /**
330      * Get the length in chars for this source
331      * @return length
332      */
333     public int getLength() {
334         return length;
335     }
336 
337     /**
338      * Read all of the source until end of file. Return it as char array
339      *
340      * @param reader  reader opened to source stream
341      * @return source as content
342      *
343      * @throws IOException if source could not be read
344      */
345     public static char[] readFully(final Reader reader) throws IOException {
346         final char[]        arr = new char[BUFSIZE];
347         final StringBuilder sb  = new StringBuilder();
348 
349         try {
350             int numChars;
351             while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
352                 sb.append(arr, 0, numChars);
353             }
354         } finally {
355             reader.close();
356         }
357 
358         return sb.toString().toCharArray();
359     }
360 
361     /**
362      * Read all of the source until end of file. Return it as char array
363      *
364      * @param file  source file
365      * @return source as content
366      *
367      * @throws IOException if source could not be read
368      */
369     public static char[] readFully(final File file) throws IOException {
370         if (!file.isFile()) {
371             throw new IOException(file + " is not a file"); //TODO localize?
372         }
373         return byteToCharArray(Files.readAllBytes(file.toPath()));
374     }
375 
376     /**
377      * Read all of the source until end of file. Return it as char array
378      *
379      * @param file  source file
380      * @param cs Charset used to convert bytes to chars
381      * @return source as content
382      *
383      * @throws IOException if source could not be read
384      */
385     public static char[] readFully(final File file, final Charset cs) throws IOException {
386         if (!file.isFile()) {
387             throw new IOException(file + " is not a file"); //TODO localize?
388         }
389 
390         final byte[] buf = Files.readAllBytes(file.toPath());
391         return (cs != null)? new String(buf, cs).toCharArray() : byteToCharArray(buf);
392     }
393 
394     /**
395      * Read all of the source until end of stream from the given URL. Return it as char array
396      *
397      * @param url URL to read content from
398      * @return source as content
399      *
400      * @throws IOException if source could not be read
401      */
402     public static char[] readFully(final URL url) throws IOException {
403         return readFully(url.openStream());
404     }
405 
406     /**
407      * Read all of the source until end of file. Return it as char array
408      *
409      * @param url URL to read content from
410      * @param cs Charset used to convert bytes to chars
411      * @return source as content
412      *
413      * @throws IOException if source could not be read
414      */
415     public static char[] readFully(final URL url, final Charset cs) throws IOException {
416         return readFully(url.openStream(), cs);
417     }
418 
419     /**
420      * Get the base url. This is currently used for testing only
421      * @param url a URL
422      * @return base URL for url
423      */
424     public static String baseURL(final URL url) {
425         return baseURL(url, null);
426     }
427 
428     private static String baseURL(final URL url, final String defaultValue) {
429         if (url.getProtocol().equals("file")) {
430             try {
431                 final Path path = Paths.get(url.toURI());
432                 final Path parent = path.getParent();
433                 return (parent != null) ? (parent + File.separator) : defaultValue;
434             } catch (final SecurityException | URISyntaxException | IOError e) {
435                 return defaultValue;
436             }
437         }
438 
439         // FIXME: is there a better way to find 'base' URL of a given URL?
440         String path = url.getPath();
441         if (path.isEmpty()) {
442             return defaultValue;
443         }
444         path = path.substring(0, path.lastIndexOf('/') + 1);
445         final int port = url.getPort();
446         try {
447             return new URL(url.getProtocol(), url.getHost(), port, path).toString();
448         } catch (final MalformedURLException e) {
449             return defaultValue;
450         }
451     }
452 
453     private static String dirName(final File file, final String defaultValue) {
454         final String res = file.getParent();
455         return (res != null)? (res + File.separator) : defaultValue;
456     }
457 
458     // fake directory like name
459     private static String baseName(final String name, final String defaultValue) {
460         int idx = name.lastIndexOf('/');
461         if (idx == -1) {
462             idx = name.lastIndexOf('\\');
463         }
464         return (idx != -1)? name.substring(0, idx + 1) : defaultValue;
465     }
466 
467     private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
468         return (cs != null)? new String(readBytes(is), cs).toCharArray() : readFully(is);
469     }
470 
471     private static char[] readFully(final InputStream is) throws IOException {
472         return byteToCharArray(readBytes(is));
473     }
474 
475     private static char[] byteToCharArray(final byte[] bytes) {
476         Charset cs = StandardCharsets.UTF_8;
477         int start = 0;
478         // BOM detection.
479         if (bytes.length > 1 && bytes[0] == (byte)0xFE && bytes[1] == (byte)0xFF) {
480             start = 2;
481             cs = StandardCharsets.UTF_16BE;
482         } else if (bytes.length > 1 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE) {
483             start = 2;
484             cs = StandardCharsets.UTF_16LE;
485         } else if (bytes.length > 2 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) {
486             start = 3;
487             cs = StandardCharsets.UTF_8;
488         } else if (bytes.length > 3 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE && bytes[2] == 0 && bytes[3] == 0) {
489             start = 4;
490             cs = Charset.forName("UTF-32LE");
491         } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte)0xFE && bytes[3] == (byte)0xFF) {
492             start = 4;
493             cs = Charset.forName("UTF-32BE");
494         }
495 
496         return new String(bytes, start, bytes.length - start, cs).toCharArray();
497     }
498 
499     static byte[] readBytes(final InputStream is) throws IOException {
500         final byte[] arr = new byte[BUFSIZE];
501         try {
502             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
503                 int numBytes;
504                 while ((numBytes = is.read(arr, 0, arr.length)) > 0) {
505                     buf.write(arr, 0, numBytes);
506                 }
507                 return buf.toByteArray();
508             }
509         } finally {
510             is.close();
511         }
512     }
513 
514     @Override
515     public String toString() {
516         return getName();
517     }
518 
519     private static URL getURLFromFile(final File file) {
520         try {
521             return file.toURI().toURL();
522         } catch (final SecurityException | MalformedURLException ignored) {
523             return null;
524         }
525     }
526 }