8040078: Avoid repeated reading of source for cached loads
authorhannesw
Fri, 25 Apr 2014 16:34:17 +0200
changeset 24206 40c6d45af73f
parent 24205 0a7fbab84fb0
child 24207 8e7a633f1d8e
8040078: Avoid repeated reading of source for cached loads Reviewed-by: jlaskey, lagergren
nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java
nashorn/src/jdk/nashorn/internal/ir/debug/JSONWriter.java
nashorn/src/jdk/nashorn/internal/objects/NativeFunction.java
nashorn/src/jdk/nashorn/internal/runtime/Context.java
nashorn/src/jdk/nashorn/internal/runtime/JSONFunctions.java
nashorn/src/jdk/nashorn/internal/runtime/Source.java
nashorn/src/jdk/nashorn/tools/Shell.java
nashorn/test/script/trusted/JDK-8006529.js
nashorn/test/src/jdk/nashorn/internal/codegen/CompilerTest.java
nashorn/test/src/jdk/nashorn/internal/parser/ParserTest.java
nashorn/test/src/jdk/nashorn/internal/runtime/ContextTest.java
nashorn/test/src/jdk/nashorn/internal/runtime/SourceTest.java
nashorn/test/src/jdk/nashorn/internal/test/framework/SharedContextEvaluator.java
nashorn/test/src/jdk/nashorn/test/models/SourceHelper.java
--- a/nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/api/scripting/NashornScriptEngine.java	Fri Apr 25 16:34:17 2014 +0200
@@ -27,16 +27,14 @@
 
 import static jdk.nashorn.internal.runtime.ECMAErrors.referenceError;
 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.lang.invoke.MethodHandles;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.net.URL;
-import java.nio.charset.Charset;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.security.Permissions;
@@ -124,21 +122,21 @@
         }
     }
 
-    // load engine.js and return content as a char[]
+    // load engine.js
     @SuppressWarnings("resource")
-    private static char[] loadEngineJSSource() {
+    private static Source loadEngineJSSource() {
         final String script = "resources/engine.js";
         try {
-            final InputStream is = AccessController.doPrivileged(
-                    new PrivilegedExceptionAction<InputStream>() {
+            return AccessController.doPrivileged(
+                    new PrivilegedExceptionAction<Source>() {
                         @Override
-                        public InputStream run() throws Exception {
+                        public Source run() throws IOException {
                             final URL url = NashornScriptEngine.class.getResource(script);
-                            return url.openStream();
+                            return sourceFor(NashornException.ENGINE_SCRIPT_SOURCE_NAME, url);
                         }
-                    });
-            return Source.readFully(new InputStreamReader(is));
-        } catch (final PrivilegedActionException | IOException e) {
+                    }
+            );
+        } catch (final PrivilegedActionException e) {
             if (Context.DEBUG) {
                 e.printStackTrace();
             }
@@ -147,7 +145,7 @@
     }
 
     // Source object for engine.js
-    private static final Source ENGINE_SCRIPT_SRC = new Source(NashornException.ENGINE_SCRIPT_SOURCE_NAME, loadEngineJSSource());
+    private static final Source ENGINE_SCRIPT_SRC = loadEngineJSSource();
 
     NashornScriptEngine(final NashornScriptEngineFactory factory, final ClassLoader appLoader) {
         this(factory, DEFAULT_OPTIONS, appLoader);
@@ -282,19 +280,14 @@
 
     private static Source makeSource(final Reader reader, final ScriptContext ctxt) throws ScriptException {
         try {
-            if (reader instanceof URLReader) {
-                final URL url = ((URLReader)reader).getURL();
-                final Charset cs = ((URLReader)reader).getCharset();
-                return new Source(url.toString(), url, cs);
-            }
-            return new Source(getScriptName(ctxt), Source.readFully(reader));
-        } catch (final IOException e) {
+            return sourceFor(getScriptName(ctxt), reader);
+        } catch (IOException e) {
             throw new ScriptException(e);
         }
     }
 
     private static Source makeSource(final String src, final ScriptContext ctxt) {
-        return new Source(getScriptName(ctxt), src);
+        return sourceFor(getScriptName(ctxt), src);
     }
 
     private static String getScriptName(final ScriptContext ctxt) {
--- a/nashorn/src/jdk/nashorn/internal/ir/debug/JSONWriter.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/internal/ir/debug/JSONWriter.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,8 @@
 
 package jdk.nashorn.internal.ir.debug;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.ArrayList;
@@ -88,7 +90,7 @@
      * @return JSON string representation of AST of the supplied code
      */
     public static String parse(final ScriptEnvironment env, final String code, final String name, final boolean includeLoc) {
-        final Parser       parser     = new Parser(env, new Source(name, code), new Context.ThrowErrorManager(), env._strict);
+        final Parser       parser     = new Parser(env, sourceFor(name, code), new Context.ThrowErrorManager(), env._strict);
         final JSONWriter   jsonWriter = new JSONWriter(includeLoc);
         try {
             final FunctionNode functionNode = parser.parse(CompilerConstants.RUN_SCRIPT.symbolName());
--- a/nashorn/src/jdk/nashorn/internal/objects/NativeFunction.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/internal/objects/NativeFunction.java	Fri Apr 25 16:34:17 2014 +0200
@@ -27,6 +27,7 @@
 
 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
 
 import java.util.List;
 
@@ -257,7 +258,7 @@
     }
 
     private static void checkFunctionParameters(final String params) {
-        final Source src = new Source("<function>", params);
+        final Source src = sourceFor("<function>", params);
         final Parser parser = new Parser(Global.getEnv(), src, new Context.ThrowErrorManager());
         try {
             parser.parseFormalParameterList();
@@ -267,7 +268,7 @@
     }
 
     private static void checkFunctionBody(final String funcBody) {
-        final Source src = new Source("<function>", funcBody);
+        final Source src = sourceFor("<function>", funcBody);
         final Parser parser = new Parser(Global.getEnv(), src, new Context.ThrowErrorManager());
         try {
             parser.parseFunctionBody();
--- a/nashorn/src/jdk/nashorn/internal/runtime/Context.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/internal/runtime/Context.java	Fri Apr 25 16:34:17 2014 +0200
@@ -32,6 +32,7 @@
 import static jdk.nashorn.internal.lookup.Lookup.MH;
 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
 
 import java.io.File;
 import java.io.IOException;
@@ -501,7 +502,7 @@
      */
     public Object eval(final ScriptObject initialScope, final String string, final Object callThis, final Object location, final boolean strict) {
         final String  file       = (location == UNDEFINED || location == null) ? "<eval>" : location.toString();
-        final Source  source     = new Source(file, string);
+        final Source  source     = sourceFor(file, string);
         final boolean directEval = location != UNDEFINED; // is this direct 'eval' call or indirectly invoked eval?
         final Global  global = Context.getGlobal();
 
@@ -568,7 +569,7 @@
                         public Source run() {
                             try {
                                 final URL resURL = Context.class.getResource(resource);
-                                return (resURL != null)? new Source(srcStr, resURL) : null;
+                                return (resURL != null)? sourceFor(srcStr, resURL) : null;
                             } catch (final IOException exp) {
                                 return null;
                             }
@@ -600,7 +601,7 @@
             final String srcStr = (String)src;
             if (srcStr.startsWith(LOAD_CLASSPATH)) {
                 URL url = getResourceURL(srcStr.substring(LOAD_CLASSPATH.length()));
-                source = (url != null)? new Source(url.toString(), url) : null;
+                source = (url != null)? sourceFor(url.toString(), url) : null;
             } else {
                 final File file = new File(srcStr);
                 if (srcStr.indexOf(':') != -1) {
@@ -613,31 +614,31 @@
                         } catch (final MalformedURLException e) {
                             url = file.toURI().toURL();
                         }
-                        source = new Source(url.toString(), url);
+                        source = sourceFor(url.toString(), url);
                     }
                 } else if (file.isFile()) {
-                    source = new Source(srcStr, file);
+                    source = sourceFor(srcStr, file);
                 }
             }
         } else if (src instanceof File && ((File)src).isFile()) {
             final File file = (File)src;
-            source = new Source(file.getName(), file);
+            source = sourceFor(file.getName(), file);
         } else if (src instanceof URL) {
             final URL url = (URL)src;
-            source = new Source(url.toString(), url);
+            source = sourceFor(url.toString(), url);
         } else if (src instanceof ScriptObject) {
             final ScriptObject sobj = (ScriptObject)src;
             if (sobj.has("script") && sobj.has("name")) {
                 final String script = JSType.toString(sobj.get("script"));
                 final String name   = JSType.toString(sobj.get("name"));
-                source = new Source(name, script);
+                source = sourceFor(name, script);
             }
         } else if (src instanceof Map) {
             final Map<?,?> map = (Map<?,?>)src;
             if (map.containsKey("script") && map.containsKey("name")) {
                 final String script = JSType.toString(map.get("script"));
                 final String name   = JSType.toString(map.get("name"));
-                source = new Source(name, script);
+                source = sourceFor(name, script);
             }
         }
 
--- a/nashorn/src/jdk/nashorn/internal/runtime/JSONFunctions.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/internal/runtime/JSONFunctions.java	Fri Apr 25 16:34:17 2014 +0200
@@ -39,6 +39,8 @@
 import jdk.nashorn.internal.runtime.arrays.ArrayIndex;
 import jdk.nashorn.internal.runtime.linker.Bootstrap;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+
 /**
  * Utilities used by "JSON" object implementation.
  */
@@ -77,9 +79,7 @@
      */
     public static Object parse(final Object text, final Object reviver) {
         final String     str     = JSType.toString(text);
-        final JSONParser parser  = new JSONParser(
-                new Source("<json>", str),
-                new Context.ThrowErrorManager());
+        final JSONParser parser  = new JSONParser(sourceFor("<json>", str), new Context.ThrowErrorManager());
 
         Node node;
 
--- a/nashorn/src/jdk/nashorn/internal/runtime/Source.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/internal/runtime/Source.java	Fri Apr 25 16:34:17 2014 +0200
@@ -27,13 +27,16 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOError;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
+import java.lang.ref.WeakReference;
 import java.net.MalformedURLException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLConnection;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -43,13 +46,19 @@
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.WeakHashMap;
+import jdk.nashorn.api.scripting.URLReader;
 import jdk.nashorn.internal.parser.Token;
 
 /**
  * Source objects track the origin of JavaScript entities.
- *
  */
 public final class Source {
+
+    private static final DebugLogger DEBUG = new DebugLogger("source");
+    private static final int BUF_SIZE = 8 * 1024;
+    private static final Cache CACHE = new Cache();
+
     /**
      * Descriptive name of the source as supplied by the user. Used for error
      * reporting to the user. For example, SyntaxError will use this to print message.
@@ -64,11 +73,8 @@
      */
     private final String base;
 
-    /** Cached source content. */
-    private final char[] content;
-
-    /** Length of source content. */
-    private final int length;
+    /** Source content */
+    private final Data data;
 
     /** Cached hash code */
     private int hash;
@@ -76,40 +82,297 @@
     /** Message digest */
     private byte[] digest;
 
-    /** Source URL if available */
-    private final URL url;
+    // Do *not* make this public, ever! Trusts the URL and content.
+    private Source(final String name, final String base, final Data data) {
+        this.name = name;
+        this.base = base;
+        this.data = data;
+    }
+
+    private static synchronized Source sourceFor(final String name, final String base, final URLData data) throws IOException {
+        try {
+            final Source newSource = new Source(name, base, data);
+            final Source existingSource = CACHE.get(newSource);
+            if (existingSource != null) {
+                // Force any access errors
+                data.checkPermissionAndClose();
+                return existingSource;
+            } else {
+                // All sources in cache must be fully loaded
+                data.load();
+                CACHE.put(newSource, newSource);
+                return newSource;
+            }
+        } catch (RuntimeException e) {
+            final Throwable cause = e.getCause();
+            if (cause instanceof IOException) {
+                throw (IOException) cause;
+            }
+            throw e;
+        }
+    }
+
+    private static class Cache extends WeakHashMap<Source, WeakReference<Source>> {
+        public Source get(final Source key) {
+            final WeakReference<Source> ref = super.get(key);
+            return ref == null ? null : ref.get();
+        }
+
+        public void put(final Source key, final Source value) {
+            assert !(value.data instanceof RawData);
+            put(key, new WeakReference<>(value));
+        }
+    }
+
+    // Wrapper to manage lazy loading
+    private static interface Data {
+
+        URL url();
+
+        int length();
+
+        long lastModified();
+
+        char[] array();
+    }
+
+    private static class RawData implements Data {
+        private final char[] array;
+        private int hash;
+
+        private RawData(final char[] array) {
+            this.array = Objects.requireNonNull(array);
+        }
 
-    private static final int BUFSIZE = 8 * 1024;
+        private RawData(final String source) {
+            this.array = Objects.requireNonNull(source).toCharArray();
+        }
+
+        private RawData(final Reader reader) throws IOException {
+            this(readFully(reader));
+        }
+
+        @Override
+        public int hashCode() {
+            int h = hash;
+            if (h == 0) {
+                h = hash = Arrays.hashCode(array);
+            }
+            return h;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof RawData) {
+                return Arrays.equals(array, ((RawData)obj).array);
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return new String(array());
+        }
+
+        @Override
+        public URL url() {
+            return null;
+        }
+
+        @Override
+        public int length() {
+            return array.length;
+        }
+
+        @Override
+        public long lastModified() {
+            return 0;
+        }
+
+        @Override
+        public char[] array() {
+            return array;
+        }
+
+
+    }
+
+    private static class URLData implements Data {
+        private final URL url;
+        protected final Charset cs;
+        private int hash;
+        protected char[] array;
+        protected int length;
+        protected long lastModified;
+
+        private URLData(final URL url, final Charset cs) {
+            this.url = Objects.requireNonNull(url);
+            this.cs = cs;
+        }
 
-    // Do *not* make this public ever! Trusts the URL and content. So has to be called
-    // from other public constructors. Note that this can not be some init method as
-    // we initialize final fields from here.
-    private Source(final String name, final String base, final char[] content, final URL url) {
-        this.name    = name;
-        this.base    = base;
-        this.content = content;
-        this.length  = content.length;
-        this.url     = url;
+        @Override
+        public int hashCode() {
+            int h = hash;
+            if (h == 0) {
+                h = hash = url.hashCode();
+            }
+            return h;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof URLData)) {
+                return false;
+            }
+
+            URLData otherData = (URLData) other;
+
+            if (url.equals(otherData.url)) {
+                // Make sure both have meta data loaded
+                try {
+                    if (isDeferred()) {
+                        // Data in cache is always loaded, and we only compare to cached data.
+                        assert !otherData.isDeferred();
+                        loadMeta();
+                    } else if (otherData.isDeferred()) {
+                        otherData.loadMeta();
+                    }
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+
+                // Compare meta data
+                return this.length == otherData.length && this.lastModified == otherData.lastModified;
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return new String(array());
+        }
+
+        @Override
+        public URL url() {
+            return url;
+        }
+
+        @Override
+        public int length() {
+            return length;
+        }
+
+        @Override
+        public long lastModified() {
+            return lastModified;
+        }
+
+        @Override
+        public char[] array() {
+            assert !isDeferred();
+            return array;
+        }
+
+        boolean isDeferred() {
+            return array == null;
+        }
+
+        protected void checkPermissionAndClose() throws IOException {
+            try (InputStream in = url.openStream()) {}
+            debug("permission checked for ", url);
+        }
+
+        protected void load() throws IOException {
+            if (array == null) {
+                final URLConnection c = url.openConnection();
+                try (InputStream in = c.getInputStream()) {
+                    array = cs == null ? readFully(in) : readFully(in, cs);
+                    length = array.length;
+                    lastModified = c.getLastModified();
+                    debug("loaded content for ", url);
+                }
+            }
+        }
+
+        protected void loadMeta() throws IOException {
+            if (length == 0 && lastModified == 0) {
+                final URLConnection c = url.openConnection();
+                length = c.getContentLength();
+                lastModified = c.getLastModified();
+                debug("loaded metadata for ", url);
+            }
+        }
+    }
+
+    private static class FileData extends URLData {
+        private final File file;
+
+        private FileData(final File file, final Charset cs) {
+            super(getURLFromFile(file), cs);
+            this.file = file;
+
+        }
+
+        @Override
+        protected void checkPermissionAndClose() throws IOException {
+            if (!file.canRead()) {
+                throw new FileNotFoundException(file + " (Permission Denied)");
+            }
+            debug("permission checked for ", file);
+        }
+
+        @Override
+        protected void loadMeta() {
+            if (length == 0 && lastModified == 0) {
+                length = (int) file.length();
+                lastModified = file.lastModified();
+                debug("loaded metadata for ", file);
+            }
+        }
+
+        @Override
+        protected void load() throws IOException {
+            if (array == null) {
+                array = cs == null ? readFully(file) : readFully(file, cs);
+                length = array.length;
+                lastModified = file.lastModified();
+                debug("loaded content for ", file);
+            }
+        }
+    }
+
+    private static void debug(final Object... msg) {
+        DEBUG.info(msg);
+    }
+
+    private char[] data() {
+        return data.array();
     }
 
     /**
-     * Constructor
+     * Returns an instance
      *
      * @param name    source name
      * @param content contents as char array
      */
-    public Source(final String name, final char[] content) {
-        this(name, baseName(name, null), content, null);
+    public static Source sourceFor(final String name, final char[] content) {
+        return new Source(name, baseName(name), new RawData(content));
     }
 
     /**
-     * Constructor
+     * Returns an instance
      *
      * @param name    source name
      * @param content contents as string
      */
-    public Source(final String name, final String content) {
-        this(name, content.toCharArray());
+    public static Source sourceFor(final String name, final String content) {
+        return new Source(name, baseName(name), new RawData(content));
     }
 
     /**
@@ -120,8 +383,8 @@
      *
      * @throws IOException if source cannot be loaded
      */
-    public Source(final String name, final URL url) throws IOException {
-        this(name, baseURL(url, null), readFully(url), url);
+    public static Source sourceFor(final String name, final URL url) throws IOException {
+        return sourceFor(name, url, null);
     }
 
     /**
@@ -133,8 +396,8 @@
      *
      * @throws IOException if source cannot be loaded
      */
-    public Source(final String name, final URL url, final Charset cs) throws IOException {
-        this(name, baseURL(url, null), readFully(url, cs), url);
+    public static Source sourceFor(final String name, final URL url, final Charset cs) throws IOException {
+        return sourceFor(name, baseURL(url), new URLData(url, cs));
     }
 
     /**
@@ -145,8 +408,8 @@
      *
      * @throws IOException if source cannot be loaded
      */
-    public Source(final String name, final File file) throws IOException {
-        this(name, dirName(file, null), readFully(file), getURLFromFile(file));
+    public static Source sourceFor(final String name, final File file) throws IOException {
+        return sourceFor(name, file, null);
     }
 
     /**
@@ -158,8 +421,25 @@
      *
      * @throws IOException if source cannot be loaded
      */
-    public Source(final String name, final File file, final Charset cs) throws IOException {
-        this(name, dirName(file, null), readFully(file, cs), getURLFromFile(file));
+    public static Source sourceFor(final String name, final File file, final Charset cs) throws IOException {
+        final File absFile = file.getAbsoluteFile();
+        return sourceFor(name, dirName(absFile, null), new FileData(file, cs));
+    }
+
+    /**
+     * Returns an instance
+     *
+     * @param name source name
+     * @param reader reader from which source can be loaded
+     * @throws IOException if source cannot be loaded
+     */
+    public static Source sourceFor(final String name, final Reader reader) throws IOException {
+        // Extract URL from URLReader to defer loading and reuse cached data if available.
+        if (reader instanceof URLReader) {
+            final URLReader urlReader = (URLReader) reader;
+            return sourceFor(name, urlReader.getURL(), urlReader.getCharset());
+        }
+        return new Source(name, baseName(name), new RawData(reader));
     }
 
     @Override
@@ -167,21 +447,18 @@
         if (this == obj) {
             return true;
         }
-
         if (!(obj instanceof Source)) {
             return false;
         }
-
-        final Source src = (Source)obj;
-        // Only compare content as a last resort measure
-        return length == src.length && Objects.equals(url, src.url) && Objects.equals(name, src.name) && Arrays.equals(content, src.content);
+        final Source other = (Source) obj;
+        return Objects.equals(name, other.name) && data.equals(other.data);
     }
 
     @Override
     public int hashCode() {
         int h = hash;
         if (h == 0) {
-            h = hash = Arrays.hashCode(content) ^ Objects.hashCode(name);
+            h = hash = data.hashCode() ^ Objects.hashCode(name);
         }
         return h;
     }
@@ -191,7 +468,7 @@
      * @return Source content.
      */
     public String getString() {
-        return new String(content, 0, length);
+        return data.toString();
     }
 
     /**
@@ -203,6 +480,14 @@
     }
 
     /**
+     * Get the last modified time of this script.
+     * @return Last modified time.
+     */
+    public long getLastModified() {
+        return data.lastModified();
+    }
+
+    /**
      * Get the "directory" part of the file or "base" of the URL.
      * @return base of file or URL.
      */
@@ -217,7 +502,7 @@
      * @return Source content portion.
      */
     public String getString(final int start, final int len) {
-        return new String(content, start, len);
+        return new String(data(), start, len);
     }
 
     /**
@@ -228,7 +513,7 @@
     public String getString(final long token) {
         final int start = Token.descPosition(token);
         final int len = Token.descLength(token);
-        return new String(content, start, len);
+        return new String(data(), start, len);
     }
 
     /**
@@ -238,7 +523,7 @@
      * @return URL source or null
      */
     public URL getURL() {
-        return url;
+        return data.url();
     }
 
     /**
@@ -247,8 +532,9 @@
      * @return Index of first character of line.
      */
     private int findBOLN(final int position) {
+        final char[] data = data();
         for (int i = position - 1; i > 0; i--) {
-            final char ch = content[i];
+            final char ch = data[i];
 
             if (ch == '\n' || ch == '\r') {
                 return i + 1;
@@ -264,8 +550,10 @@
      * @return Index of last character of line.
      */
     private int findEOLN(final int position) {
-         for (int i = position; i < length; i++) {
-            final char ch = content[i];
+        final char[] data = data();
+        final int length = data.length;
+        for (int i = position; i < length; i++) {
+            final char ch = data[i];
 
             if (ch == '\n' || ch == '\r') {
                 return i - 1;
@@ -285,11 +573,12 @@
      * @return Line number.
      */
     public int getLine(final int position) {
+        final char[] data = data();
         // Line count starts at 1.
         int line = 1;
 
         for (int i = 0; i < position; i++) {
-            final char ch = content[i];
+            final char ch = data[i];
             // Works for both \n and \r\n.
             if (ch == '\n') {
                 line++;
@@ -320,7 +609,7 @@
         // Find end of this line.
         final int last = findEOLN(position);
 
-        return new String(content, first, last - first + 1);
+        return new String(data(), first, last - first + 1);
     }
 
     /**
@@ -328,7 +617,7 @@
      * @return content
      */
     public char[] getContent() {
-        return content.clone();
+        return data().clone();
     }
 
     /**
@@ -336,19 +625,18 @@
      * @return length
      */
     public int getLength() {
-        return length;
+        return data.length();
     }
 
     /**
      * Read all of the source until end of file. Return it as char array
      *
-     * @param reader  reader opened to source stream
+     * @param reader reader opened to source stream
      * @return source as content
-     *
      * @throws IOException if source could not be read
      */
     public static char[] readFully(final Reader reader) throws IOException {
-        final char[]        arr = new char[BUFSIZE];
+        final char[]        arr = new char[BUF_SIZE];
         final StringBuilder sb  = new StringBuilder();
 
         try {
@@ -366,9 +654,8 @@
     /**
      * Read all of the source until end of file. Return it as char array
      *
-     * @param file  source file
+     * @param file source file
      * @return source as content
-     *
      * @throws IOException if source could not be read
      */
     public static char[] readFully(final File file) throws IOException {
@@ -381,10 +668,9 @@
     /**
      * Read all of the source until end of file. Return it as char array
      *
-     * @param file  source file
+     * @param file source file
      * @param cs Charset used to convert bytes to chars
      * @return source as content
-     *
      * @throws IOException if source could not be read
      */
     public static char[] readFully(final File file, final Charset cs) throws IOException {
@@ -393,7 +679,7 @@
         }
 
         final byte[] buf = Files.readAllBytes(file.toPath());
-        return (cs != null)? new String(buf, cs).toCharArray() : byteToCharArray(buf);
+        return (cs != null) ? new String(buf, cs).toCharArray() : byteToCharArray(buf);
     }
 
     /**
@@ -401,7 +687,6 @@
      *
      * @param url URL to read content from
      * @return source as content
-     *
      * @throws IOException if source could not be read
      */
     public static char[] readFully(final URL url) throws IOException {
@@ -414,7 +699,6 @@
      * @param url URL to read content from
      * @param cs Charset used to convert bytes to chars
      * @return source as content
-     *
      * @throws IOException if source could not be read
      */
     public static char[] readFully(final URL url, final Charset cs) throws IOException {
@@ -428,7 +712,7 @@
      */
     public synchronized byte[] getDigest() {
         if (digest == null) {
-
+            final char[] content = data();
             final byte[] bytes = new byte[content.length * 2];
 
             for (int i = 0; i < content.length; i++) {
@@ -444,8 +728,8 @@
                 if (base != null) {
                     md.update(base.getBytes(StandardCharsets.UTF_8));
                 }
-                if (url != null) {
-                    md.update(url.toString().getBytes(StandardCharsets.UTF_8));
+                if (getURL() != null) {
+                    md.update(getURL().toString().getBytes(StandardCharsets.UTF_8));
                 }
                 digest = md.digest(bytes);
             } catch (NoSuchAlgorithmException e) {
@@ -461,50 +745,46 @@
      * @return base URL for url
      */
     public static String baseURL(final URL url) {
-        return baseURL(url, null);
-    }
-
-    private static String baseURL(final URL url, final String defaultValue) {
         if (url.getProtocol().equals("file")) {
             try {
                 final Path path = Paths.get(url.toURI());
                 final Path parent = path.getParent();
-                return (parent != null) ? (parent + File.separator) : defaultValue;
+                return (parent != null) ? (parent + File.separator) : null;
             } catch (final SecurityException | URISyntaxException | IOError e) {
-                return defaultValue;
+                return null;
             }
         }
 
         // FIXME: is there a better way to find 'base' URL of a given URL?
         String path = url.getPath();
         if (path.isEmpty()) {
-            return defaultValue;
+            return null;
         }
         path = path.substring(0, path.lastIndexOf('/') + 1);
         final int port = url.getPort();
         try {
             return new URL(url.getProtocol(), url.getHost(), port, path).toString();
         } catch (final MalformedURLException e) {
-            return defaultValue;
+            return null;
         }
     }
 
-    private static String dirName(final File file, final String defaultValue) {
+    private static String dirName(final File file, final String DEFAULT_BASE_NAME) {
         final String res = file.getParent();
-        return (res != null)? (res + File.separator) : defaultValue;
+        return (res != null) ? (res + File.separator) : DEFAULT_BASE_NAME;
     }
 
     // fake directory like name
-    private static String baseName(final String name, final String defaultValue) {
+    private static String baseName(final String name) {
         int idx = name.lastIndexOf('/');
         if (idx == -1) {
             idx = name.lastIndexOf('\\');
         }
-        return (idx != -1)? name.substring(0, idx + 1) : defaultValue;
+        return (idx != -1) ? name.substring(0, idx + 1) : null;
     }
 
     private static char[] readFully(final InputStream is, final Charset cs) throws IOException {
-        return (cs != null)? new String(readBytes(is), cs).toCharArray() : readFully(is);
+        return (cs != null) ? new String(readBytes(is), cs).toCharArray() : readFully(is);
     }
 
     private static char[] readFully(final InputStream is) throws IOException {
@@ -515,19 +795,19 @@
         Charset cs = StandardCharsets.UTF_8;
         int start = 0;
         // BOM detection.
-        if (bytes.length > 1 && bytes[0] == (byte)0xFE && bytes[1] == (byte)0xFF) {
+        if (bytes.length > 1 && bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) {
             start = 2;
             cs = StandardCharsets.UTF_16BE;
-        } else if (bytes.length > 1 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE) {
+        } else if (bytes.length > 1 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) {
             start = 2;
             cs = StandardCharsets.UTF_16LE;
-        } else if (bytes.length > 2 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) {
+        } else if (bytes.length > 2 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
             start = 3;
             cs = StandardCharsets.UTF_8;
-        } else if (bytes.length > 3 && bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE && bytes[2] == 0 && bytes[3] == 0) {
+        } else if (bytes.length > 3 && bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE && bytes[2] == 0 && bytes[3] == 0) {
             start = 4;
             cs = Charset.forName("UTF-32LE");
-        } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte)0xFE && bytes[3] == (byte)0xFF) {
+        } else if (bytes.length > 3 && bytes[0] == 0 && bytes[1] == 0 && bytes[2] == (byte) 0xFE && bytes[3] == (byte) 0xFF) {
             start = 4;
             cs = Charset.forName("UTF-32BE");
         }
@@ -536,7 +816,7 @@
     }
 
     static byte[] readBytes(final InputStream is) throws IOException {
-        final byte[] arr = new byte[BUFSIZE];
+        final byte[] arr = new byte[BUF_SIZE];
         try {
             try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
                 int numBytes;
--- a/nashorn/src/jdk/nashorn/tools/Shell.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/src/jdk/nashorn/tools/Shell.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,8 @@
 
 package jdk.nashorn.tools;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -50,7 +52,6 @@
 import jdk.nashorn.internal.runtime.Property;
 import jdk.nashorn.internal.runtime.ScriptEnvironment;
 import jdk.nashorn.internal.runtime.ScriptFunction;
-import jdk.nashorn.internal.runtime.ScriptObject;
 import jdk.nashorn.internal.runtime.ScriptRuntime;
 import jdk.nashorn.internal.runtime.Source;
 import jdk.nashorn.internal.runtime.options.Options;
@@ -244,7 +245,7 @@
 
             // For each file on the command line.
             for (final String fileName : files) {
-                final FunctionNode functionNode = new Parser(env, new Source(fileName, new File(fileName)), errors).parse();
+                final FunctionNode functionNode = new Parser(env, sourceFor(fileName, new File(fileName)), errors).parse();
 
                 if (errors.getNumberOfErrors() != 0) {
                     return COMPILATION_ERROR;
@@ -302,7 +303,7 @@
                 }
 
                 final File file = new File(fileName);
-                final ScriptFunction script = context.compileScript(new Source(fileName, file.toURI().toURL()), global);
+                final ScriptFunction script = context.compileScript(sourceFor(fileName, file), global);
                 if (script == null || errors.getNumberOfErrors() != 0) {
                     return COMPILATION_ERROR;
                 }
@@ -405,7 +406,7 @@
 
             // initialize with "shell.js" script
             try {
-                final Source source = new Source("<shell.js>", Shell.class.getResource("resources/shell.js"));
+                final Source source = sourceFor("<shell.js>", Shell.class.getResource("resources/shell.js"));
                 context.eval(global, source.getString(), global, "<shell.js>", false);
             } catch (final Exception e) {
                 err.println(e);
--- a/nashorn/test/script/trusted/JDK-8006529.js	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/script/trusted/JDK-8006529.js	Fri Apr 25 16:34:17 2014 +0200
@@ -113,7 +113,7 @@
 var getContextMethod = Context.class.getMethod("getContext")
 var getEnvMethod = Context.class.getMethod("getEnv")
 
-var SourceConstructor = Source.class.getConstructor(java.lang.String.class, java.lang.String.class)
+var sourceForMethod = Source.class.getMethod("sourceFor", java.lang.String.class, java.lang.String.class)
 var ParserConstructor = Parser.class.getConstructor(ScriptEnvironment.class, Source.class, ErrorManager.class)
 var CompilerConstructor = Compiler.class.getConstructor(ScriptEnvironment.class)
 
@@ -121,7 +121,7 @@
 // source code, returns a jdk.nashorn.internal.ir.FunctionNode object 
 // representing it.
 function compile(source) {
-    var source = SourceConstructor.newInstance("<no name>", source);
+    var source = sourceForMethod.invoke(null, "<no name>", source);
 
     var env = getEnvMethod.invoke(getContextMethod.invoke(null))
 
--- a/nashorn/test/src/jdk/nashorn/internal/codegen/CompilerTest.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/src/jdk/nashorn/internal/codegen/CompilerTest.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,9 @@
 
 package jdk.nashorn.internal.codegen;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+import static jdk.nashorn.internal.runtime.Source.readFully;
+
 import java.io.File;
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -32,7 +35,6 @@
 import jdk.nashorn.internal.runtime.Context;
 import jdk.nashorn.internal.runtime.ErrorManager;
 import jdk.nashorn.internal.runtime.ScriptFunction;
-import jdk.nashorn.internal.runtime.ScriptObject;
 import jdk.nashorn.internal.runtime.Source;
 import jdk.nashorn.internal.runtime.options.Options;
 import org.testng.Assert;
@@ -151,7 +153,7 @@
         final boolean globalChanged = (oldGlobal != global);
 
         try {
-            final char[] buffer = Source.readFully(file);
+            final char[] buffer = readFully(file);
             boolean excluded = false;
 
             if (filter != null) {
@@ -170,7 +172,7 @@
             if (globalChanged) {
                 Context.setGlobal(global);
             }
-            final Source source = new Source(file.getAbsolutePath(), buffer);
+            final Source source = sourceFor(file.getAbsolutePath(), buffer);
             final ScriptFunction script = context.compileScript(source, global);
             if (script == null || context.getErrorManager().getNumberOfErrors() > 0) {
                 log("Compile failed: " + file.getAbsolutePath());
--- a/nashorn/test/src/jdk/nashorn/internal/parser/ParserTest.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/src/jdk/nashorn/internal/parser/ParserTest.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,9 @@
 
 package jdk.nashorn.internal.parser;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+import static jdk.nashorn.internal.runtime.Source.readFully;
+
 import java.io.File;
 import jdk.nashorn.internal.runtime.Context;
 import jdk.nashorn.internal.runtime.ErrorManager;
@@ -131,7 +134,7 @@
         }
 
         try {
-            final char[] buffer = Source.readFully(file);
+            final char[] buffer = readFully(file);
             boolean excluded = false;
             if (filter != null) {
                 final String content = new String(buffer);
@@ -153,7 +156,7 @@
                 }
             };
             errors.setLimit(0);
-            final Source   source   = new Source(file.getAbsolutePath(), buffer);
+            final Source   source   = sourceFor(file.getAbsolutePath(), buffer);
             new Parser(context.getEnv(), source, errors).parse();
             if (errors.getNumberOfErrors() > 0) {
                 log("Parse failed: " + file.getAbsolutePath());
--- a/nashorn/test/src/jdk/nashorn/internal/runtime/ContextTest.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/src/jdk/nashorn/internal/runtime/ContextTest.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,7 @@
 
 package jdk.nashorn.internal.runtime;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
@@ -107,7 +108,7 @@
     }
 
     private Object eval(final Context cx, final String name, final String code) {
-        final Source source = new Source(name, code);
+        final Source source = sourceFor(name, code);
         final ScriptObject global = Context.getGlobal();
         final ScriptFunction func = cx.compileScript(source, global);
         return func != null ? ScriptRuntime.apply(func, global) : null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/src/jdk/nashorn/internal/runtime/SourceTest.java	Fri Apr 25 16:34:17 2014 +0200
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nashorn.internal.runtime;
+
+import jdk.nashorn.api.scripting.URLReader;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.util.Arrays;
+
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+/**
+ * Tests different Source representations.
+ */
+public class SourceTest {
+
+    final private static String SOURCE_NAME = "source.js";
+    final private static String SOURCE_STRING = "var x = 1;";
+    final private static char[] SOURCE_CHARS = SOURCE_STRING.toCharArray();
+    final private static String RESOURCE_PATH = "resources/load_test.js";
+    final private static File SOURCE_FILE = new File("build/test/classes/jdk/nashorn/internal/runtime/" + RESOURCE_PATH);
+    final private static URL  SOURCE_URL = SourceTest.class.getResource(RESOURCE_PATH);
+
+
+    @Test
+    public void testStringSource() {
+        testSources(sourceFor(SOURCE_NAME, SOURCE_STRING), sourceFor(SOURCE_NAME, SOURCE_STRING));
+        testSources(sourceFor(SOURCE_NAME, SOURCE_STRING), sourceFor(SOURCE_NAME, SOURCE_CHARS));
+    }
+
+    @Test
+    public void testCharArraySource() {
+        testSources(sourceFor(SOURCE_NAME, SOURCE_CHARS), sourceFor(SOURCE_NAME, SOURCE_CHARS));
+        testSources(sourceFor(SOURCE_NAME, SOURCE_CHARS), sourceFor(SOURCE_NAME, SOURCE_STRING));
+    }
+
+    @Test
+    public void testURLSource() {
+        try {
+            testSources(sourceFor(SOURCE_NAME, SOURCE_URL), sourceFor(SOURCE_NAME, SOURCE_URL));
+            testSources(sourceFor(SOURCE_NAME, SOURCE_URL), sourceFor(SOURCE_NAME, new URLReader(SOURCE_URL)));
+
+        } catch (final IOException e) {
+            fail(e.toString());
+        }
+    }
+
+    @Test
+    public void testURLReaderSource() {
+        try {
+            System.err.println(SourceTest.class.getResource(""));
+            testSources(sourceFor(SOURCE_NAME, new URLReader(SOURCE_URL)), sourceFor(SOURCE_NAME, new URLReader(SOURCE_URL)));
+            testSources(sourceFor(SOURCE_NAME, new URLReader(SOURCE_URL)), sourceFor(SOURCE_NAME, SOURCE_URL));
+        } catch (final IOException e) {
+            fail(e.toString());
+        }
+    }
+
+    @Test
+    public void testReaderSource() {
+        try {
+            testSources(sourceFor(SOURCE_NAME, getReader(RESOURCE_PATH)), sourceFor(SOURCE_NAME, getReader(RESOURCE_PATH)));
+        } catch (final IOException e) {
+            fail(e.toString());
+        }
+    }
+
+    @Test
+    public void testFileSource() {
+        try {
+            testSources(sourceFor(SOURCE_NAME, SOURCE_FILE), sourceFor(SOURCE_NAME, SOURCE_FILE));
+        } catch (final IOException e) {
+            fail(e.toString());
+        }
+    }
+
+    private Reader getReader(final String path) {
+        return new InputStreamReader(SourceTest.class.getResourceAsStream(path));
+    }
+
+    private void testSources(final Source source1, final Source source2) {
+        final char[] chars1 = source1.getContent();
+        final char[] chars2 = source2.getContent();
+        final String str1 = source1.getString();
+        final String str2 = source2.getString();
+        assertTrue(Arrays.equals(chars1, chars2));
+        assertEquals(str1, str2);
+        assertEquals(source1.hashCode(), source2.hashCode());
+        assertTrue(source1.equals(source2));
+        // Test for immutability
+        Arrays.fill(source1.getContent(), (char)0);
+        Arrays.fill(source2.getContent(), (char)1);
+        assertTrue(Arrays.equals(source1.getContent(), str1.toCharArray()));
+        assertTrue(Arrays.equals(source1.getContent(), chars1));
+        assertTrue(Arrays.equals(source1.getContent(), source2.getContent()));
+    }
+}
--- a/nashorn/test/src/jdk/nashorn/internal/test/framework/SharedContextEvaluator.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/src/jdk/nashorn/internal/test/framework/SharedContextEvaluator.java	Fri Apr 25 16:34:17 2014 +0200
@@ -25,6 +25,7 @@
 
 package jdk.nashorn.internal.test.framework;
 
+import static jdk.nashorn.internal.runtime.Source.sourceFor;
 import static jdk.nashorn.tools.Shell.COMPILATION_ERROR;
 import static jdk.nashorn.tools.Shell.RUNTIME_ERROR;
 import static jdk.nashorn.tools.Shell.SUCCESS;
@@ -39,7 +40,6 @@
 import jdk.nashorn.internal.runtime.ErrorManager;
 import jdk.nashorn.internal.runtime.ScriptFunction;
 import jdk.nashorn.internal.runtime.ScriptRuntime;
-import jdk.nashorn.internal.runtime.Source;
 import jdk.nashorn.internal.runtime.options.Options;
 
 /**
@@ -125,7 +125,7 @@
                     continue;
                 }
                 final File file = new File(fileName);
-                ScriptFunction script = context.compileScript(new Source(fileName, file.toURI().toURL()), global);
+                ScriptFunction script = context.compileScript(sourceFor(fileName, file.toURI().toURL()), global);
 
                 if (script == null || errors.getNumberOfErrors() != 0) {
                     return COMPILATION_ERROR;
--- a/nashorn/test/src/jdk/nashorn/test/models/SourceHelper.java	Fri Apr 25 14:20:07 2014 +0200
+++ b/nashorn/test/src/jdk/nashorn/test/models/SourceHelper.java	Fri Apr 25 16:34:17 2014 +0200
@@ -46,7 +46,7 @@
     }
 
     public static String readFully(final URL url) throws IOException {
-        return new Source(url.toString(), url).getString();
+        return Source.sourceFor(url.toString(), url).getString();
     }
 
     public static String readFully(final Reader reader) throws IOException {