changeset 4956:bb696d27c662

RT-29584: Implement window.localStorage
author Vasiliy Baranov <vasiliy.baranov@oracle.com>
date Wed, 04 Sep 2013 13:53:41 +0400
parents fc56181afdd6
children dc4e8f0255ba
files modules/web/src/main/java/com/sun/webkit/FileSystem.java modules/web/src/main/java/com/sun/webkit/WebPage.java modules/web/src/main/java/javafx/scene/web/DirectoryLock.java modules/web/src/main/java/javafx/scene/web/WebEngine.java modules/web/src/main/java/javafx/scene/web/WebErrorEvent.java modules/web/src/main/native/Source/WebCore/TargetJava.pri modules/web/src/main/native/Source/WebCore/mapfile-macosx modules/web/src/main/native/Source/WebCore/mapfile-vers modules/web/src/main/native/Source/WebCore/platform/java/FileSystemJava.cpp modules/web/src/main/native/Source/WebCore/platform/java/FrameLoaderClientJava.cpp modules/web/src/main/native/Source/WebCore/platform/java/WebPage.cpp modules/web/src/main/native/Source/WebCore/storage/StorageStrategy.cpp modules/web/src/main/native/Source/WebCore/storage/StorageThread.cpp modules/web/src/main/native/Source/WebCore/storage/StorageThread.h modules/web/src/main/native/Source/WebCore/storage/java/StorageAreaJava.cpp modules/web/src/main/native/Source/WebCore/storage/java/StorageAreaJava.h modules/web/src/main/native/Source/WebCore/storage/java/StorageNamespaceJava.cpp modules/web/src/main/native/Source/WebCore/storage/java/StorageNamespaceJava.h modules/web/src/test/java/javafx/scene/web/DirectoryLockTest.java modules/web/src/test/java/javafx/scene/web/LoadNotificationsTest.java modules/web/src/test/java/javafx/scene/web/UserDataDirectoryTest.java
diffstat 21 files changed, 1834 insertions(+), 650 deletions(-) [+]
line wrap: on
line diff
--- a/modules/web/src/main/java/com/sun/webkit/FileSystem.java	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/java/com/sun/webkit/FileSystem.java	Wed Sep 04 13:53:41 2013 +0400
@@ -4,7 +4,11 @@
 package com.sun.webkit;
 
 import java.io.File;
+import java.io.IOException;
 import static java.lang.String.format;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Paths;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -18,6 +22,9 @@
         throw new AssertionError();
     }
 
+    private static boolean fwkFileExists(String path) {
+        return new File(path).exists();
+    }
 
     private static long fwkGetFileSize(String path) {
         try {
@@ -32,6 +39,23 @@
         return -1;
     }
 
+    private static String fwkPathByAppendingComponent(String path,
+                                                      String component)
+    {
+        return new File(path, component).getPath();
+    }
+
+    private static boolean fwkMakeAllDirectories(String path) {
+        try {
+            Files.createDirectories(Paths.get(path));
+            return true;
+        } catch (InvalidPathException|IOException ex) {
+            logger.log(Level.FINE, format("Error creating "
+                    + "directory [%s]", path), ex);
+            return false;
+        }
+    }
+
     private static String fwkPathGetFileName(String path) {
         return new File(path).getName();
     }
--- a/modules/web/src/main/java/com/sun/webkit/WebPage.java	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/java/com/sun/webkit/WebPage.java	Wed Sep 04 13:53:41 2013 +0400
@@ -1900,6 +1900,24 @@
         }
     }
 
+    public void setLocalStorageDatabasePath(String path) {
+        lockPage();
+        try {
+            twkSetLocalStorageDatabasePath(getPage(), path);
+        } finally {
+            unlockPage();
+        }
+    }
+
+    public void setLocalStorageEnabled(boolean enabled) {
+        lockPage();
+        try {
+            twkSetLocalStorageEnabled(getPage(), enabled);
+        } finally {
+            unlockPage();
+        }
+    }
+
     // ---- INSPECTOR SUPPORT ---- //
 
     public void connectInspectorFrontend() {
@@ -2488,6 +2506,8 @@
     private native void twkSetUserStyleSheetLocation(long page, String url);
     private native String twkGetUserAgent(long page);
     private native void twkSetUserAgent(long page, String userAgent);
+    private native void twkSetLocalStorageDatabasePath(long page, String path);
+    private native void twkSetLocalStorageEnabled(long page, boolean enabled);
 
     private native int twkGetUnloadEventListenersCount(long pFrame);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/main/java/javafx/scene/web/DirectoryLock.java	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ */
+package javafx.scene.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import static java.lang.String.format;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+final class DirectoryLock {
+
+    private static final Logger logger =
+            Logger.getLogger(DirectoryLock.class.getName());
+    private static final Map<File,Descriptor> descriptors = new HashMap<>();
+
+
+    private Descriptor descriptor;
+
+
+    DirectoryLock(File directory) throws IOException,
+                                         DirectoryAlreadyInUseException
+    {
+        directory = canonicalize(directory);
+        descriptor = descriptors.get(directory);
+        if (descriptor == null) {
+            File lockFile = lockFile(directory);
+            RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
+            try {
+                FileLock lock = lockRaf.getChannel().tryLock();
+                if (lock == null) {
+                    throw new DirectoryAlreadyInUseException(
+                            directory.toString(), null);
+                }
+                descriptor = new Descriptor(directory, lockRaf, lock);
+                descriptors.put(directory, descriptor);
+            } catch (OverlappingFileLockException ex) {
+                throw new DirectoryAlreadyInUseException(
+                        directory.toString(), ex);
+            } finally {
+                if (descriptor == null) { // tryLock failed
+                    try {
+                        lockRaf.close();
+                    } catch (IOException ex) {
+                        logger.log(Level.WARNING, format("Error closing [%s]",
+                                lockFile), ex);
+                    }
+                }
+            }
+        }
+        descriptor.referenceCount++;
+    }
+
+
+    void close() {
+        if (descriptor == null) {
+            return;
+        }
+        descriptor.referenceCount--;
+        if (descriptor.referenceCount == 0) {
+            try {
+                descriptor.lock.release();
+            } catch (IOException ex) {
+                logger.log(Level.WARNING, format("Error releasing "
+                        + "lock on [%s]", lockFile(descriptor.directory)), ex);
+            }
+            try {
+                descriptor.lockRaf.close();
+            } catch (IOException ex) {
+                logger.log(Level.WARNING, format("Error closing [%s]",
+                        lockFile(descriptor.directory)), ex);
+            }
+            descriptors.remove(descriptor.directory);
+        }
+        descriptor = null;
+    }
+
+
+    static int referenceCount(File directory) throws IOException {
+        Descriptor d = descriptors.get(canonicalize(directory));
+        return d == null ? 0 : d.referenceCount;
+    }
+
+    static File canonicalize(File directory) throws IOException {
+        String path = directory.getCanonicalPath();
+        if (path.length() > 0
+                && path.charAt(path.length() - 1) != File.separatorChar)
+        {
+            path += File.separatorChar;
+        }
+        return new File(path);
+    }
+
+    private static File lockFile(File directory) {
+        return new File(directory, ".lock");
+    }
+
+
+    private static class Descriptor {
+        private final File directory;
+        private final RandomAccessFile lockRaf;
+        private final FileLock lock;
+        private int referenceCount;
+
+        private Descriptor(File directory,
+                           RandomAccessFile lockRaf,
+                           FileLock lock)
+        {
+            this.directory = directory;
+            this.lockRaf = lockRaf;
+            this.lock = lock;
+        }
+    }
+
+    final class DirectoryAlreadyInUseException extends Exception {
+        DirectoryAlreadyInUseException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+}
--- a/modules/web/src/main/java/javafx/scene/web/WebEngine.java	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/java/javafx/scene/web/WebEngine.java	Wed Sep 04 13:53:41 2013 +0400
@@ -28,6 +28,7 @@
 import javafx.beans.property.*;
 import javafx.concurrent.Worker;
 import javafx.event.EventHandler;
+import javafx.event.EventType;
 import javafx.geometry.Rectangle2D;
 import javafx.print.PageLayout;
 import javafx.print.PrinterJob;
@@ -37,12 +38,20 @@
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
+import static java.lang.String.format;
 import java.lang.ref.WeakReference;
 import java.net.MalformedURLException;
 import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import static com.sun.webkit.LoadListenerClient.*;
 
@@ -267,6 +276,9 @@
         Utilities.setUtilities(new UtilitiesImpl());
     }
 
+    private static final Logger logger =
+            Logger.getLogger(WebEngine.class.getName());
+
     /**
      * The number of instances of this class.
      * Used to start and stop the pulse timer.
@@ -290,11 +302,13 @@
      */
     private final WebPage page;
 
-    /**
-     * The debugger associated with this web engine.
-     */
+    private final SelfDisposer disposer;
+
     private final DebuggerImpl debugger = new DebuggerImpl();
 
+    private boolean userDataDirectoryApplied = false;
+
+
     /**
      * Returns a {@link javafx.concurrent.Worker} object that can be used to
      * track loading progress.
@@ -474,7 +488,58 @@
         }
         return userStyleSheetLocation;
     }
-    
+
+    /**
+     * Specifies the directory to be used by this {@code WebEngine}
+     * to store local user data.
+     *
+     * <p>If the value of this property is not {@code null},
+     * the {@code WebEngine} will attempt to store local user data
+     * in the respective directory.
+     * If the value of this property is {@code null},
+     * the {@code WebEngine} will attempt to store local user data
+     * in an automatically selected system-dependent user- and
+     * application-specific directory.
+     *
+     * <p>When a {@code WebEngine} is about to start loading a web
+     * page or executing a script for the first time, it checks whether
+     * it can actually use the directory specified by this property.
+     * If the check fails for some reason, the {@code WebEngine} invokes
+     * the {@link WebEngine#onErrorProperty WebEngine.onError} event handler,
+     * if any, with a {@link WebErrorEvent} describing the reason.
+     * If the invoked event handler modifies the {@code userDataDirectory}
+     * property, the {@code WebEngine} retries with the new value as soon
+     * as the handler returns. If the handler does not modify the
+     * {@code userDataDirectory} property (which is the default),
+     * the {@code WebEngine} continues without local user data.
+     *
+     * <p>Once the {@code WebEngine} has started loading a web page or
+     * executing a script, changes made to this property have no effect
+     * on where the {@code WebEngine} stores or will store local user
+     * data.
+     *
+     * <p>Currently, the directory specified by this property is used
+     * only to store the data that backs the {@code window.localStorage}
+     * objects. In the future, more types of data can be added.
+     * 
+     * @defaultValue {@code null}
+     * @since JavaFX 8.0
+     */
+    private final ObjectProperty<File> userDataDirectory =
+            new SimpleObjectProperty<>(this, "userDataDirectory");
+
+    public final File getUserDataDirectory() {
+        return userDataDirectory.get();
+    }
+
+    public final void setUserDataDirectory(File value) {
+        userDataDirectory.set(value);
+    }
+
+    public final ObjectProperty<File> userDataDirectoryProperty() {
+        return userDataDirectory;
+    }
+
     /**
      * Specifies user agent ID string. This string is the value of the
      * {@code User-Agent} HTTP header.
@@ -704,21 +769,43 @@
     public final ObjectProperty<Callback<PromptData, String>> promptHandlerProperty() { return promptHandler; }
 
     /**
+     * The event handler called when an error occurs.
+     *
+     * @defaultValue {@code null}
+     * @since JavaFX 8.0
+     */
+    private final ObjectProperty<EventHandler<WebErrorEvent>> onError =
+            new SimpleObjectProperty<>(this, "onError");
+
+    public final EventHandler<WebErrorEvent> getOnError() {
+        return onError.get();
+    }
+
+    public final void setOnError(EventHandler<WebErrorEvent> handler) {
+        onError.set(handler);
+    }
+
+    public final ObjectProperty<EventHandler<WebErrorEvent>> onErrorProperty() {
+        return onError;
+    }
+
+
+    /**
      * Creates a new engine.
      */
     public WebEngine() {
-        this(null);
+        this(null, false);
     }
 
     /**
      * Creates a new engine and loads a Web page into it.
      */
     public WebEngine(String url) {
-        if (instanceCount == 0 &&
-            Timer.getMode() == Timer.Mode.PLATFORM_TICKS)
-        {
-            PulseTimer.start();
-        }
+        this(url, true);
+    }
+
+    private WebEngine(String url, boolean callLoad) {
+        checkThread();
         Accessor accessor = new AccessorImpl(this);
         page = new WebPage(
             new WebPageClientImpl(accessor),
@@ -731,10 +818,18 @@
         
         history = new WebHistory(page);
 
-        Disposer.addRecord(this, new SelfDisposer(page));
+        disposer = new SelfDisposer(page);
+        Disposer.addRecord(this, disposer);
 
-        load(url);
-        
+        if (callLoad) {
+            load(url);
+        }
+
+        if (instanceCount == 0 &&
+            Timer.getMode() == Timer.Mode.PLATFORM_TICKS)
+        {
+            PulseTimer.start();
+        }
         instanceCount++;
     }
 
@@ -762,6 +857,7 @@
                 return;
             }
         }
+        applyUserDataDirectory();
         page.open(page.getMainFrame(), url);
     }
 
@@ -786,6 +882,7 @@
     public void loadContent(String content, String contentType) {
         checkThread();
         loadWorker.cancelAndReset();
+        applyUserDataDirectory();
         page.load(page.getMainFrame(), content, contentType);
     }
 
@@ -833,6 +930,7 @@
      */
     public Object executeScript(String script) {
         checkThread();
+        applyUserDataDirectory();
         return page.executeScript(page.getMainFrame(), script);
     }
 
@@ -853,15 +951,136 @@
         page.stop(page.getMainFrame());
     }
 
+    private void applyUserDataDirectory() {
+        if (userDataDirectoryApplied) {
+            return;
+        }
+        userDataDirectoryApplied = true;
+        File nominalUserDataDir = getUserDataDirectory();
+        while (true) {
+            File userDataDir;
+            String displayString;
+            if (nominalUserDataDir == null) {
+                userDataDir = defaultUserDataDirectory();
+                displayString = format("null (%s)", userDataDir);
+            } else {
+                userDataDir = nominalUserDataDir;
+                displayString = userDataDir.toString();
+            }
+            logger.log(Level.FINE, "Trying to apply user data "
+                    + "directory [{0}]", displayString);
+            String errorMessage;
+            EventType<WebErrorEvent> errorType;
+            Throwable error;
+            try {
+                userDataDir = DirectoryLock.canonicalize(userDataDir);
+                File localStorageDir = new File(userDataDir, "localstorage");
+                File[] dirs = new File[] {
+                    userDataDir,
+                    localStorageDir,
+                };
+                for (File dir : dirs) {
+                    createDirectories(dir);
+                    // Additional security check to make sure the caller
+                    // has permission to write to the target directory
+                    File test = new File(dir, ".test");
+                    if (test.createNewFile()) {
+                        test.delete();
+                    }
+                }
+                disposer.userDataDirectoryLock = new DirectoryLock(userDataDir);
+
+                page.setLocalStorageDatabasePath(localStorageDir.getPath());
+                page.setLocalStorageEnabled(true);
+
+                logger.log(Level.FINE, "User data directory [{0}] has "
+                        + "been applied successfully", displayString);
+                return;
+
+            } catch (DirectoryLock.DirectoryAlreadyInUseException ex) {
+                errorMessage = "User data directory [%s] is already in use";
+                errorType = WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE;
+                error = ex;
+            } catch (IOException ex) {
+                errorMessage = "An I/O error occurred while setting up "
+                        + "user data directory [%s]";
+                errorType = WebErrorEvent.USER_DATA_DIRECTORY_IO_ERROR;
+                error = ex;
+            } catch (SecurityException ex) {
+                errorMessage = "A security error occurred while setting up "
+                        + "user data directory [%s]";
+                errorType = WebErrorEvent.USER_DATA_DIRECTORY_SECURITY_ERROR;
+                error = ex;
+            }
+
+            errorMessage = format(errorMessage, displayString);
+            logger.log(Level.FINE, "{0}, calling error handler", errorMessage);
+            File oldNominalUserDataDir = nominalUserDataDir;
+            fireError(errorType, errorMessage, error);
+            nominalUserDataDir = getUserDataDirectory();
+            if (Objects.equals(nominalUserDataDir, oldNominalUserDataDir)) {
+                logger.log(Level.FINE, "Error handler did not "
+                        + "modify user data directory, continuing "
+                        + "without user data directory");
+                return;
+            } else {
+                logger.log(Level.FINE, "Error handler has set "
+                        + "user data directory to [{0}], "
+                        + "retrying", nominalUserDataDir);
+                continue;
+            }
+        }
+    }
+
+    private static File defaultUserDataDirectory() {
+        return new File(
+                com.sun.glass.ui.Application.GetApplication()
+                        .getDataDirectory(),
+                "webview");
+    }
+
+    private static void createDirectories(File directory) throws IOException {
+        Path path = directory.toPath();
+        try {
+            Files.createDirectories(path, PosixFilePermissions.asFileAttribute(
+                    PosixFilePermissions.fromString("rwx------")));
+        } catch (UnsupportedOperationException ex) {
+            Files.createDirectories(path);
+        }
+    }
+
+    private void fireError(EventType<WebErrorEvent> eventType, String message,
+                           Throwable exception)
+    {
+        EventHandler<WebErrorEvent> handler = getOnError();
+        if (handler != null) {
+            handler.handle(new WebErrorEvent(this, eventType,
+                                             message, exception));
+        }
+    }
+
+    // for testing purposes only
+    void dispose() {
+        disposer.dispose();
+    }
+
     private static final class SelfDisposer implements DisposerRecord {
-        private final WebPage page;
+        private WebPage page;
+        private DirectoryLock userDataDirectoryLock;
 
         private SelfDisposer(WebPage page) {
             this.page = page;
         }
 
         @Override public void dispose() {
+            if (page == null) {
+                return;
+            }
             page.dispose();
+            page = null;
+            if (userDataDirectoryLock != null) {
+                userDataDirectoryLock.close();
+            }
             instanceCount--;
             if (instanceCount == 0 &&
                 Timer.getMode() == Timer.Mode.PLATFORM_TICKS)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/main/java/javafx/scene/web/WebErrorEvent.java	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ */
+package javafx.scene.web;
+
+import javafx.event.Event;
+import javafx.event.EventType;
+
+/**
+ * An event indicating a {@link WebEngine} error.
+ * Holds an optional text message and an optional exception
+ * associated with the error.
+ *
+ * @see WebEngine#onErrorProperty WebEngine.onError
+ * @since JavaFX 8.0
+ */
+public final class WebErrorEvent extends Event {
+
+    /**
+     * Common supertype for all {@code WebErrorEvent} types.
+     */
+    public static final EventType<WebErrorEvent> ANY =
+            new EventType<WebErrorEvent>(Event.ANY, "WEB_ERROR");
+
+    /**
+     * This event occurs when a {@link WebEngine} detects that its
+     * user data directory is already in use by a {@code WebEngine}
+     * running in a different VM.
+     *
+     * <p>In general, multiple {@code WebEngine} instances may share
+     * a single user data directory as long as they run in the same
+     * VM. {@code WebEngine} instances running in different VMs are
+     * not allowed to share the same user data directory.
+     *
+     * <p>When a {@code WebEngine} is about to start loading a web
+     * page or executing a script for the first time, it checks whether
+     * its {@link WebEngine#userDataDirectoryProperty userDataDirectory}
+     * is already in use by a {@code WebEngine} running in a different
+     * VM. If the latter is the case, the {@code WebEngine} invokes the
+     * {@link WebEngine#onErrorProperty WebEngine.onError} event handler,
+     * if any, with a {@code USER_DATA_DIRECTORY_ALREADY_IN_USE} event.
+     * If the invoked event handler modifies the {@code userDataDirectory}
+     * property, the {@code WebEngine} retries with the new user
+     * data directory as soon as the handler returns. If the handler
+     * does not modify the {@code userDataDirectory} property (which
+     * is the default), the {@code WebEngine} continues without the
+     * user data directory.
+     */
+    public static final EventType<WebErrorEvent>
+            USER_DATA_DIRECTORY_ALREADY_IN_USE = new EventType<WebErrorEvent>(
+                    WebErrorEvent.ANY, "USER_DATA_DIRECTORY_ALREADY_IN_USE");
+
+    /**
+     * This event occurs when a {@link WebEngine} encounters
+     * an I/O error while trying to create or access the user
+     * data directory.
+     *
+     * <p>When a {@code WebEngine} is about to start loading a web
+     * page or executing a script for the first time, it checks whether
+     * it can create or access its
+     * {@link WebEngine#userDataDirectoryProperty userDataDirectory}.
+     * If the check fails with an I/O error (such as {@code
+     * java.io.IOException}), the {@code WebEngine} invokes the {@link
+     * WebEngine#onErrorProperty WebEngine.onError} event handler,
+     * if any, with a {@code USER_DATA_DIRECTORY_IO_ERROR} event.
+     * If the invoked event handler modifies the {@code userDataDirectory}
+     * property, the {@code WebEngine} retries with the new user
+     * data directory as soon as the handler returns. If the handler
+     * does not modify the {@code userDataDirectory} property (which
+     * is the default), the {@code WebEngine} continues without the
+     * user data directory.
+     */
+    public static final EventType<WebErrorEvent>
+            USER_DATA_DIRECTORY_IO_ERROR = new EventType<WebErrorEvent>(
+                    WebErrorEvent.ANY, "USER_DATA_DIRECTORY_IO_ERROR");
+
+    /**
+     * This event occurs when a {@link WebEngine} encounters
+     * a security error while trying to create or access the user
+     * data directory.
+     *
+     * <p>When a {@code WebEngine} is about to start loading a web
+     * page or executing a script for the first time, it checks whether
+     * it can create or access its
+     * {@link WebEngine#userDataDirectoryProperty userDataDirectory}.
+     * If the check fails with a security error (such as {@code
+     * java.lang.SecurityException}), the {@code WebEngine} invokes the
+     * {@link WebEngine#onErrorProperty WebEngine.onError} event handler,
+     * if any, with a {@code USER_DATA_DIRECTORY_SECURITY_ERROR} event.
+     * If the invoked event handler modifies the {@code userDataDirectory}
+     * property, the {@code WebEngine} retries with the new user
+     * data directory as soon as the handler returns. If the handler
+     * does not modify the {@code userDataDirectory} property (which
+     * is the default), the {@code WebEngine} continues without the
+     * user data directory.
+     */
+    public static final EventType<WebErrorEvent>
+            USER_DATA_DIRECTORY_SECURITY_ERROR = new EventType<WebErrorEvent>(
+                    WebErrorEvent.ANY, "USER_DATA_DIRECTORY_SECURITY_ERROR");
+
+
+    private final String message;
+    private final Throwable exception;
+
+
+    /**
+     * Creates a new {@code WebErrorEvent}.
+     * @param source the event source which sent the event
+     * @param eventType the event type
+     * @param message the text message associated with the event;
+     *        may be {@code null}
+     * @param exception the exception associated with the event;
+     *        may be {@code null}
+     */
+    public WebErrorEvent(Object source, EventType<WebErrorEvent> type,
+                         String message, Throwable exception)
+    {
+        super(source, null, type);
+        this.message = message;
+        this.exception = exception;
+    }
+
+
+    /**
+     * Returns the text message associated with this event.
+     * @return the text message associated with this event, or
+     *         {@code null} if there is no such message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * Returns the exception associated with this event.
+     * @return the exception associated with this event, or
+     *         {@code null} if there is no such exception
+     */
+    public Throwable getException() {
+        return exception;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return String.format("WebErrorEvent [source = %s, eventType = %s, "
+                + "message = \"%s\", exception = %s]",
+                getSource(), getEventType(), getMessage(), getException());
+    }
+}
--- a/modules/web/src/main/native/Source/WebCore/TargetJava.pri	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/TargetJava.pri	Wed Sep 04 13:53:41 2013 +0400
@@ -129,6 +129,18 @@
     $$PWD/platform/image-decoders/webp
 }
 
+INCLUDEPATH += $(WEBKITOUTPUTDIR)/import/include
+LIBS += -L$(WEBKITOUTPUTDIR)/import/lib -lsqlite3
+
+# The following line ensures that __STDC_FORMAT_MACROS is defined when
+# <inttypes.h> is included in the precompiled header. Note that the file
+# that actually requires <inttypes.h>'s format macros, SQLiteFileSystem.cpp,
+# does include <inttypes.h> and define __STDC_FORMAT_MACROS prior to that.
+# The problem is that with certain versions of GCC the SQLiteFileSystem.cpp's
+# include has no effect because <inttypes.h> is already included in the
+# precompiled header.
+QMAKE_CXXFLAGS += -D__STDC_FORMAT_MACROS
+
 win32-* {
     QMAKE_CXXFLAGS += -DLIBXML_STATIC
     LIBS += -llibxml2_a -lole32 -ladvapi32 -luser32
@@ -442,6 +454,7 @@
     Modules/notifications/WorkerContextNotifications.cpp \
     Modules/proximity/DeviceProximityController.cpp \
     Modules/proximity/DeviceProximityEvent.cpp \
+    Modules/webdatabase/DatabaseAuthorizer.cpp \
     css/BasicShapeFunctions.cpp \
     css/CSSAspectRatioValue.cpp \
     css/CSSBasicShapes.cpp \
@@ -1373,12 +1386,12 @@
     platform/ScrollView.cpp \
     platform/SharedBuffer.cpp \
     platform/SharedBufferChunkReader.cpp \
-#    platform/sql/SQLiteAuthorizer.cpp \
-#    platform/sql/SQLiteDatabase.cpp \
-#    platform/sql/SQLiteFileSystem.cpp \
-#    platform/sql/SQLiteStatement.cpp \
-#    platform/sql/SQLiteTransaction.cpp \
-#    platform/sql/SQLValue.cpp \
+    platform/sql/SQLValue.cpp \
+    platform/sql/SQLiteAuthorizer.cpp \
+    platform/sql/SQLiteDatabase.cpp \
+    platform/sql/SQLiteFileSystem.cpp \
+    platform/sql/SQLiteStatement.cpp \
+    platform/sql/SQLiteTransaction.cpp \
     platform/text/SegmentedString.cpp \
     platform/text/TextBoundaries.cpp \
     platform/text/TextBreakIterator.cpp \
@@ -1548,14 +1561,18 @@
 #    storage/Database.cpp \
 #    storage/DatabaseAuthorizer.cpp \
 #    storage/DatabaseSync.cpp \
-#    storage/LocalStorageTask.cpp \
-#    storage/LocalStorageThread.cpp \
-#    storage/Storage.cpp \
-#    storage/StorageAreaImpl.cpp \
-#    storage/StorageAreaSync.cpp \
-#    storage/StorageEvent.cpp \
-#    storage/StorageEventDispatcher.cpp \
-#    storage/StorageMap.cpp \
+    storage/Storage.cpp \
+    storage/StorageAreaImpl.cpp \
+    storage/StorageAreaSync.cpp \
+    storage/StorageEvent.cpp \
+    storage/StorageEventDispatcher.cpp \
+    storage/StorageMap.cpp \
+    storage/StorageNamespace.cpp \
+    storage/StorageNamespaceImpl.cpp \
+    storage/StorageStrategy.cpp \
+    storage/StorageSyncManager.cpp \
+    storage/StorageThread.cpp \
+    storage/StorageTracker.cpp \
     testing/Internals.cpp \
     testing/InternalSettings.cpp \
     xml/DOMParser.cpp \
@@ -1574,13 +1591,6 @@
     loader/appcache/ApplicationCacheResource.cpp \
     loader/appcache/ApplicationCacheGroup.cpp \
     loader/appcache/ManifestParser.cpp \
-    storage/Storage.cpp \
-    storage/StorageEvent.cpp \
-    storage/StorageMap.cpp \
-    storage/StorageNamespace.cpp \
-    storage/StorageStrategy.cpp \
-    storage/java/StorageNamespaceJava.cpp \
-    storage/java/StorageAreaJava.cpp \
 
 
 contains(DEFINES, ENABLE_XML=1) {
--- a/modules/web/src/main/native/Source/WebCore/mapfile-macosx	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/mapfile-macosx	Wed Sep 04 13:53:41 2013 +0400
@@ -1909,6 +1909,8 @@
                _Java_com_sun_webkit_WebPage_twkSetEditable
                _Java_com_sun_webkit_WebPage_twkSetEncoding
                _Java_com_sun_webkit_WebPage_twkSetJavaScriptEnabled
+               _Java_com_sun_webkit_WebPage_twkSetLocalStorageDatabasePath
+               _Java_com_sun_webkit_WebPage_twkSetLocalStorageEnabled
                _Java_com_sun_webkit_WebPage_twkSetTransparent
                _Java_com_sun_webkit_WebPage_twkSetUsePageCache
                _Java_com_sun_webkit_WebPage_twkSetUserAgent
--- a/modules/web/src/main/native/Source/WebCore/mapfile-vers	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/mapfile-vers	Wed Sep 04 13:53:41 2013 +0400
@@ -1909,6 +1909,8 @@
                Java_com_sun_webkit_WebPage_twkSetEditable;
                Java_com_sun_webkit_WebPage_twkSetEncoding;
                Java_com_sun_webkit_WebPage_twkSetJavaScriptEnabled;
+               Java_com_sun_webkit_WebPage_twkSetLocalStorageDatabasePath;
+               Java_com_sun_webkit_WebPage_twkSetLocalStorageEnabled;
                Java_com_sun_webkit_WebPage_twkSetTransparent;
                Java_com_sun_webkit_WebPage_twkSetUsePageCache;
                Java_com_sun_webkit_WebPage_twkSetUserAgent;
--- a/modules/web/src/main/native/Source/WebCore/platform/java/FileSystemJava.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/platform/java/FileSystemJava.cpp	Wed Sep 04 13:53:41 2013 +0400
@@ -16,10 +16,23 @@
 
 namespace WebCore {
 
-bool fileExists(const String&)
+bool fileExists(const String& path)
 {
-    notImplemented();
-    return false;
+    JNIEnv* env = WebCore_GetJavaEnv();
+
+    static jmethodID mid = env->GetStaticMethodID(
+            GetFileSystemClass(env),
+            "fwkFileExists",
+            "(Ljava/lang/String;)Z");
+    ASSERT(mid);
+
+    jboolean result = env->CallStaticBooleanMethod(
+            GetFileSystemClass(env),
+            mid,
+            (jstring)path.toJavaString(env));
+    CheckAndClearException(env);
+
+    return jbool_to_bool(result);
 }
 
 bool deleteFile(const String&)
@@ -66,14 +79,41 @@
 
 String pathByAppendingComponent(const String& path, const String& component)
 {
-    notImplemented();
-    return path + "/" + component;
+    JNIEnv* env = WebCore_GetJavaEnv();
+
+    static jmethodID mid = env->GetStaticMethodID(
+            GetFileSystemClass(env),
+            "fwkPathByAppendingComponent",
+            "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
+    ASSERT(mid);
+
+    JLString result = static_cast<jstring>(env->CallStaticObjectMethod(
+            GetFileSystemClass(env),
+            mid,
+            (jstring)path.toJavaString(env),
+            (jstring)component.toJavaString(env)));
+    CheckAndClearException(env);
+
+    return String(env, result);
 }
 
 bool makeAllDirectories(const String& path)
 {
-    notImplemented();
-    return false;
+    JNIEnv* env = WebCore_GetJavaEnv();
+
+    static jmethodID mid = env->GetStaticMethodID(
+            GetFileSystemClass(env),
+            "fwkMakeAllDirectories",
+            "(Ljava/lang/String;)Z");
+    ASSERT(mid);
+
+    jboolean result = env->CallStaticBooleanMethod(
+            GetFileSystemClass(env),
+            mid,
+            (jstring)path.toJavaString(env));
+    CheckAndClearException(env);
+
+    return jbool_to_bool(result);
 }
 
 String homeDirectoryPath()
--- a/modules/web/src/main/native/Source/WebCore/platform/java/FrameLoaderClientJava.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/platform/java/FrameLoaderClientJava.cpp	Wed Sep 04 13:53:41 2013 +0400
@@ -159,6 +159,7 @@
 void FrameLoaderClientJava::frameLoaderDestroyed()
 {
     JNIEnv* env = WebCore_GetJavaEnv();
+    initRefs(env);
 
     ASSERT(m_webPage);
     ASSERT(m_frame);
--- a/modules/web/src/main/native/Source/WebCore/platform/java/WebPage.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/platform/java/WebPage.cpp	Wed Sep 04 13:53:41 2013 +0400
@@ -820,7 +820,7 @@
 
     JSGlobalContextRef globalContext = getGlobalContext(frame->script());
     JSContextGroupRef contextGroup = JSContextGetGroup(globalContext);
-    JSContextGroupSetExecutionTimeLimit(contextGroup, 10, 0, 0);
+    JSContextGroupSetExecutionTimeLimit(contextGroup, 10, 0, 0);
 }
 
 JNIEXPORT void JNICALL Java_com_sun_webkit_WebPage_twkDestroyPage
@@ -2089,6 +2089,26 @@
     page->settings()->setUserAgent(String(env, userAgent));
 }
 
+JNIEXPORT void JNICALL Java_com_sun_webkit_WebPage_twkSetLocalStorageDatabasePath
+  (JNIEnv* env, jobject, jlong pPage, jstring path)
+{
+    ASSERT(pPage);
+    Page* page = WebPage::pageFromJLong(pPage);
+    ASSERT(page);
+    Settings* settings = page->settings();
+    settings->setLocalStorageDatabasePath(String(env, path));
+}
+
+JNIEXPORT void JNICALL Java_com_sun_webkit_WebPage_twkSetLocalStorageEnabled
+  (JNIEnv*, jobject, jlong pPage, jboolean enabled)
+{
+    ASSERT(pPage);
+    Page* page = WebPage::pageFromJLong(pPage);
+    ASSERT(page);
+    Settings* settings = page->settings();
+    settings->setLocalStorageEnabled(jbool_to_bool(enabled));
+}
+
 JNIEXPORT jboolean JNICALL Java_com_sun_webkit_WebPage_twkGetDeveloperExtrasEnabled
   (JNIEnv *, jobject, jlong pPage)
 {
--- a/modules/web/src/main/native/Source/WebCore/storage/StorageStrategy.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/main/native/Source/WebCore/storage/StorageStrategy.cpp	Wed Sep 04 13:53:41 2013 +0400
@@ -26,12 +26,7 @@
 #include "config.h"
 #include "StorageStrategy.h"
 
-#if PLATFORM(JAVA)
-#include "StorageNamespaceJava.h"
-#define StorageNamespaceImpl StorageNamespaceJava
-#else
 #include "StorageNamespaceImpl.h"
-#endif
 
 namespace WebCore {
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/main/native/Source/WebCore/storage/StorageThread.cpp	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2008 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "StorageThread.h"
+
+#include "StorageAreaSync.h"
+#include <wtf/AutodrainedPool.h>
+#include <wtf/HashSet.h>
+#include <wtf/MainThread.h>
+
+#if PLATFORM(JAVA)
+#include <jni.h>
+extern JavaVM* jvm;
+#endif
+
+namespace WebCore {
+
+static HashSet<StorageThread*>& activeStorageThreads()
+{
+    ASSERT(isMainThread());
+    DEFINE_STATIC_LOCAL(HashSet<StorageThread*>, threads, ());
+    return threads;
+}
+
+PassOwnPtr<StorageThread> StorageThread::create()
+{
+    return adoptPtr(new StorageThread);
+}
+
+StorageThread::StorageThread()
+    : m_threadID(0)
+{
+    ASSERT(isMainThread());
+}
+
+StorageThread::~StorageThread()
+{
+    ASSERT(isMainThread());
+    ASSERT(!m_threadID);
+}
+
+bool StorageThread::start()
+{
+    ASSERT(isMainThread());
+    if (!m_threadID)
+        m_threadID = createThread(StorageThread::threadEntryPointCallback, this, "WebCore: LocalStorage");
+    activeStorageThreads().add(this);
+    return m_threadID;
+}
+
+void StorageThread::threadEntryPointCallback(void* thread)
+{
+    static_cast<StorageThread*>(thread)->threadEntryPoint();
+}
+
+void StorageThread::threadEntryPoint()
+{
+#if PLATFORM(JAVA)
+    {
+        void* env;
+        jvm->AttachCurrentThreadAsDaemon(&env, 0);
+    }
+#endif
+
+    ASSERT(!isMainThread());
+
+    while (OwnPtr<Function<void ()> > function = m_queue.waitForMessage()) {
+        AutodrainedPool pool;
+        (*function)();
+    }
+
+#if PLATFORM(JAVA)
+    jvm->DetachCurrentThread();
+#endif
+}
+
+void StorageThread::dispatch(const Function<void ()>& function)
+{
+    ASSERT(isMainThread());
+    ASSERT(!m_queue.killed() && m_threadID);
+    m_queue.append(adoptPtr(new Function<void ()>(function)));
+}
+
+void StorageThread::terminate()
+{
+    ASSERT(isMainThread());
+    ASSERT(!m_queue.killed() && m_threadID);
+    activeStorageThreads().remove(this);
+    // Even in weird, exceptional cases, don't wait on a nonexistent thread to terminate.
+    if (!m_threadID)
+        return;
+
+    m_queue.append(adoptPtr(new Function<void ()>((bind(&StorageThread::performTerminate, this)))));
+    waitForThreadCompletion(m_threadID);
+    ASSERT(m_queue.killed());
+    m_threadID = 0;
+}
+
+void StorageThread::performTerminate()
+{
+    ASSERT(!isMainThread());
+    m_queue.kill();
+}
+
+void StorageThread::releaseFastMallocFreeMemoryInAllThreads()
+{
+    HashSet<StorageThread*>& threads = activeStorageThreads();
+
+    for (HashSet<StorageThread*>::iterator it = threads.begin(), end = threads.end(); it != end; ++it)
+        (*it)->dispatch(bind(WTF::releaseFastMallocFreeMemory));
+}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/main/native/Source/WebCore/storage/StorageThread.h	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2008 Apple Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef StorageThread_h
+#define StorageThread_h
+
+#include <wtf/Functional.h>
+#include <wtf/HashSet.h>
+#include <wtf/MessageQueue.h>
+#include <wtf/PassOwnPtr.h>
+#include <wtf/PassRefPtr.h>
+#include <wtf/Threading.h>
+
+namespace WebCore {
+
+class StorageAreaSync;
+class StorageTask;
+
+class StorageThread {
+    WTF_MAKE_NONCOPYABLE(StorageThread); WTF_MAKE_FAST_ALLOCATED;
+public:
+    static PassOwnPtr<StorageThread> create();
+    ~StorageThread();
+
+    bool start();
+    void terminate();
+
+    void dispatch(const Function<void()>&);
+
+    static void releaseFastMallocFreeMemoryInAllThreads();
+
+private:
+    StorageThread();
+
+    // Called on background thread.
+    static void threadEntryPointCallback(void*);
+    void threadEntryPoint();
+
+    // Background thread part of the terminate procedure.
+    void performTerminate();
+
+    ThreadIdentifier m_threadID;
+    MessageQueue<Function<void ()> > m_queue;
+};
+
+} // namespace WebCore
+
+#endif // StorageThread_h
--- a/modules/web/src/main/native/Source/WebCore/storage/java/StorageAreaJava.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,294 +0,0 @@
-/*
- * Copyright (c) 2012-2013, Oracle and/or its affiliates. All rights reserved.
- */
-#include "config.h"
-#include "StorageAreaJava.h"
-
-#include "Document.h"
-#include "ExceptionCode.h"
-#include "Frame.h"
-#include "Page.h"
-#include "SchemeRegistry.h"
-#include "SecurityOrigin.h"
-#include "Settings.h"
-//#include "StorageAreaSync.h"
-//#include "StorageEventDispatcher.h"
-#include "StorageMap.h"
-//#include "StorageSyncManager.h"
-//#include "StorageTracker.h"
-#include <wtf/MainThread.h>
-
-namespace WebCore {
-
-StorageAreaJava::~StorageAreaJava()
-{
-    ASSERT(isMainThread());
-}
-
-inline StorageAreaJava::StorageAreaJava(StorageType storageType, PassRefPtr<SecurityOrigin> origin, /*PassRefPtr<StorageSyncManager> syncManager,*/ unsigned quota)
-    : m_storageType(storageType)
-    , m_securityOrigin(origin)
-    , m_storageMap(StorageMap::create(quota))
-//    , m_storageSyncManager(syncManager)
-#ifndef NDEBUG
-    , m_isShutdown(false)
-#endif
-    , m_accessCount(0)
-    , m_closeDatabaseTimer(this, &StorageAreaJava::closeDatabaseTimerFired)
-{
-    ASSERT(isMainThread());
-    ASSERT(m_securityOrigin);
-    ASSERT(m_storageMap);
-    
-    // Accessing the shared global StorageTracker when a StorageArea is created 
-    // ensures that the tracker is properly initialized before anyone actually needs to use it.
-    //StorageTracker::tracker();
-}
-
-PassRefPtr<StorageAreaJava> StorageAreaJava::create(StorageType storageType, PassRefPtr<SecurityOrigin> origin, /*PassRefPtr<StorageSyncManager> syncManager,*/ unsigned quota)
-{
-    RefPtr<StorageAreaJava> area = adoptRef(new StorageAreaJava(storageType, origin, /*syncManager,*/ quota));
-
-    // FIXME: If there's no backing storage for LocalStorage, the default WebKit behavior should be that of private browsing,
-    // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894
-/*
-    if (area->m_storageSyncManager) {
-        area->m_storageAreaSync = StorageAreaSync::create(area->m_storageSyncManager, area.get(), area->m_securityOrigin->databaseIdentifier());
-        ASSERT(area->m_storageAreaSync);
-    }
-*/
-    return area.release();
-}
-
-PassRefPtr<StorageAreaJava> StorageAreaJava::copy()
-{
-    ASSERT(!m_isShutdown);
-    return adoptRef(new StorageAreaJava(this));
-}
-
-StorageAreaJava::StorageAreaJava(StorageAreaJava* area)
-    : m_storageType(area->m_storageType)
-    , m_securityOrigin(area->m_securityOrigin)
-    , m_storageMap(area->m_storageMap)
-//    , m_storageSyncManager(area->m_storageSyncManager)
-#ifndef NDEBUG
-    , m_isShutdown(area->m_isShutdown)
-#endif
-    , m_accessCount(0)
-    , m_closeDatabaseTimer(this, &StorageAreaJava::closeDatabaseTimerFired)
-{
-    ASSERT(isMainThread());
-    ASSERT(m_securityOrigin);
-    ASSERT(m_storageMap);
-    ASSERT(!m_isShutdown);
-}
-
-bool StorageAreaJava::canAccessStorage(Frame* frame)
-{
-    return frame && frame->page();
-}
-
-StorageType StorageAreaJava::storageType() const
-{
-    return m_storageType;
-}
-
-unsigned StorageAreaJava::length()
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    return m_storageMap->length();
-}
-
-String StorageAreaJava::key(unsigned index)
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    return m_storageMap->key(index);
-}
-
-String StorageAreaJava::item(const String& key)
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    return m_storageMap->getItem(key);
-}
-
-void StorageAreaJava::setItem(Frame* sourceFrame, const String& key, const String& value, bool& quotaException)
-{
-    ASSERT(!m_isShutdown);
-    ASSERT(!value.isNull());
-    blockUntilImportComplete();
-
-    String oldValue;
-    RefPtr<StorageMap> newMap = m_storageMap->setItem(key, value, oldValue, quotaException);
-    if (newMap)
-        m_storageMap = newMap.release();
-
-    if (quotaException)
-        return;
-
-    if (oldValue == value)
-        return;
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleItemForSync(key, value);
-    dispatchStorageEvent(key, oldValue, value, sourceFrame);
-*/
-}
-
-void StorageAreaJava::removeItem(Frame* sourceFrame, const String& key)
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    String oldValue;
-    RefPtr<StorageMap> newMap = m_storageMap->removeItem(key, oldValue);
-    if (newMap)
-        m_storageMap = newMap.release();
-
-    if (oldValue.isNull())
-        return;
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleItemForSync(key, String());
-    dispatchStorageEvent(key, oldValue, String(), sourceFrame);
-*/
-}
-
-void StorageAreaJava::clear(Frame* sourceFrame)
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    if (!m_storageMap->length())
-        return;
-
-    unsigned quota = m_storageMap->quota();
-    m_storageMap = StorageMap::create(quota);
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleClear();
-    dispatchStorageEvent(String(), String(), String(), sourceFrame);
-*/
-}
-
-bool StorageAreaJava::contains(const String& key)
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-
-    return m_storageMap->contains(key);
-}
-
-void StorageAreaJava::importItems(const HashMap<String, String>& items)
-{
-    ASSERT(!m_isShutdown);
-
-    m_storageMap->importItems(items);
-}
-
-void StorageAreaJava::close()
-{
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleFinalSync();
-*/
-#ifndef NDEBUG
-    m_isShutdown = true;
-#endif
-}
-
-void StorageAreaJava::clearForOriginDeletion()
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-    
-    if (m_storageMap->length()) {
-        unsigned quota = m_storageMap->quota();
-        m_storageMap = StorageMap::create(quota);
-    }
-/*
-    if (m_storageAreaSync) {
-        m_storageAreaSync->scheduleClear();
-        m_storageAreaSync->scheduleCloseDatabase();
-    }
-*/
-}
-    
-void StorageAreaJava::sync()
-{
-    ASSERT(!m_isShutdown);
-    blockUntilImportComplete();
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleSync();
-*/
-}
-
-void StorageAreaJava::blockUntilImportComplete() const
-{
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->blockUntilImportComplete();
-*/
-}
-
-size_t StorageAreaJava::memoryBytesUsedByCache()
-{
-    return 0;
-}
-
-void StorageAreaJava::incrementAccessCount()
-{
-    m_accessCount++;
-
-    if (m_closeDatabaseTimer.isActive())
-        m_closeDatabaseTimer.stop();
-}
-
-void StorageAreaJava::decrementAccessCount()
-{
-    ASSERT(m_accessCount);
-    --m_accessCount;
-
-    if (!m_accessCount) {
-        if (m_closeDatabaseTimer.isActive())
-            m_closeDatabaseTimer.stop();
-        m_closeDatabaseTimer.startOneShot(/*StorageTracker::tracker().storageDatabaseIdleInterval()*/ 0);
-    }
-}
-
-void StorageAreaJava::closeDatabaseTimerFired(Timer<StorageAreaJava> *)
-{
-    blockUntilImportComplete();
-/*
-    if (m_storageAreaSync)
-        m_storageAreaSync->scheduleCloseDatabase();
-*/
-}
-
-void StorageAreaJava::closeDatabaseIfIdle()
-{
-    if (m_closeDatabaseTimer.isActive()) {
-        ASSERT(!m_accessCount);
-        m_closeDatabaseTimer.stop();
-
-        closeDatabaseTimerFired(&m_closeDatabaseTimer);
-}
-}
-
-void StorageAreaJava::dispatchStorageEvent(const String& key, const String& oldValue, const String& newValue, Frame* sourceFrame)
-{
-/*
-    if (m_storageType == LocalStorage)
-        StorageEventDispatcher::dispatchLocalStorageEvents(key, oldValue, newValue, m_securityOrigin.get(), sourceFrame);
-    else
-        StorageEventDispatcher::dispatchSessionStorageEvents(key, oldValue, newValue, m_securityOrigin.get(), sourceFrame);
-*/
-}
-
-} // namespace WebCore
--- a/modules/web/src/main/native/Source/WebCore/storage/java/StorageAreaJava.h	Wed Sep 04 12:14:42 2013 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-#ifndef StorageAreaJava_h
-#define StorageAreaJava_h
-
-#include "StorageArea.h"
-#include "Timer.h"
-
-#include <wtf/HashMap.h>
-#include <wtf/PassRefPtr.h>
-#include <wtf/RefPtr.h>
-
-namespace WebCore {
-
-    class SecurityOrigin;
-    class StorageMap;
-    //class StorageAreaSync;
-
-    class StorageAreaJava : public StorageArea {
-    public:
-        static PassRefPtr<StorageAreaJava> create(StorageType, PassRefPtr<SecurityOrigin>, /*PassRefPtr<StorageSyncManager>,*/ unsigned quota);
-        virtual ~StorageAreaJava();
-
-        virtual unsigned length() OVERRIDE;
-        virtual String key(unsigned index) OVERRIDE;
-        virtual String item(const String& key) OVERRIDE;
-        virtual void setItem(Frame* sourceFrame, const String& key, const String& value, bool& quotaException) OVERRIDE;
-        virtual void removeItem(Frame* sourceFrame, const String& key) OVERRIDE;
-        virtual void clear(Frame* sourceFrame) OVERRIDE;
-        virtual bool contains(const String& key) OVERRIDE;
-
-        virtual bool canAccessStorage(Frame* sourceFrame) OVERRIDE;
-        virtual StorageType storageType() const OVERRIDE;
-
-        virtual size_t memoryBytesUsedByCache() OVERRIDE;
-
-        virtual void incrementAccessCount();
-        virtual void decrementAccessCount();
-        virtual void closeDatabaseIfIdle();
-
-        PassRefPtr<StorageAreaJava> copy();
-        void close();
-
-        // Only called from a background thread.
-        void importItems(const HashMap<String, String>& items);
-
-        // Used to clear a StorageArea and close db before backing db file is deleted.
-        void clearForOriginDeletion();
-
-        void sync();
-
-    private:
-        StorageAreaJava(StorageType, PassRefPtr<SecurityOrigin>, /*PassRefPtr<StorageSyncManager>,*/ unsigned quota);
-        explicit StorageAreaJava(StorageAreaJava*);
-
-        void blockUntilImportComplete() const;
-        void closeDatabaseTimerFired(Timer<StorageAreaJava>*);
-
-        void dispatchStorageEvent(const String& key, const String& oldValue, const String& newValue, Frame* sourceFrame);
-
-        StorageType m_storageType;
-        RefPtr<SecurityOrigin> m_securityOrigin;
-        RefPtr<StorageMap> m_storageMap;
-
-//        RefPtr<StorageAreaSync> m_storageAreaSync;
-//        RefPtr<StorageSyncManager> m_storageSyncManager;
-
-#ifndef NDEBUG
-        bool m_isShutdown;
-#endif
-        unsigned m_accessCount;
-        Timer<StorageAreaJava> m_closeDatabaseTimer;
-    };
-
-} // namespace WebCore
-
-#endif // StorageAreaJava_h
--- a/modules/web/src/main/native/Source/WebCore/storage/java/StorageNamespaceJava.cpp	Wed Sep 04 12:14:42 2013 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,174 +0,0 @@
-/*
- * Copyright (c) 2012-2013, Oracle and/or its affiliates. All rights reserved.
- */
-
-#include "config.h"
-#include "StorageNamespaceJava.h"
-
-#include "GroupSettings.h"
-#include "Page.h"
-#include "PageGroup.h"
-#include "SecurityOriginHash.h"
-#include "Settings.h"
-#include "StorageAreaJava.h"
-#include "StorageMap.h"
-//#include "StorageSyncManager.h"
-//#include "StorageTracker.h"
-#include <wtf/MainThread.h>
-#include <wtf/StdLibExtras.h>
-#include <wtf/text/StringHash.h>
-
-namespace WebCore {
-
-typedef HashMap<String, StorageNamespace*> LocalStorageNamespaceMap;
-
-static LocalStorageNamespaceMap& localStorageNamespaceMap()
-{
-    DEFINE_STATIC_LOCAL(LocalStorageNamespaceMap, localStorageNamespaceMap, ());
-    return localStorageNamespaceMap;
-}
-
-PassRefPtr<StorageNamespace> StorageNamespaceJava::localStorageNamespace(PageGroup* pageGroup)
-{
-    // Need a page in this page group to query the settings for the local storage database path.
-    // Having these parameters attached to the page settings is unfortunate since these settings are
-    // not per-page (and, in fact, we simply grab the settings from some page at random), but
-    // at this point we're stuck with it.
-    Page* page = *pageGroup->pages().begin();
-    const String& path = page->settings()->localStorageDatabasePath();
-    unsigned quota = pageGroup->groupSettings()->localStorageQuotaBytes();
-    const String lookupPath = path.isNull() ? emptyString() : path;
-
-    LocalStorageNamespaceMap::AddResult result = localStorageNamespaceMap().add(lookupPath, 0);
-    if (!result.isNewEntry)
-        return result.iterator->value;
-
-        RefPtr<StorageNamespace> storageNamespace = adoptRef(new StorageNamespaceJava(LocalStorage, lookupPath, quota));
-
-    result.iterator->value = storageNamespace.get();
-        return storageNamespace.release();
-    }
-
-PassRefPtr<StorageNamespace> StorageNamespaceJava::sessionStorageNamespace(Page* page)
-{
-    return adoptRef(new StorageNamespaceJava(SessionStorage, String(), page->settings()->sessionStorageQuota()));
-}
-
-PassRefPtr<StorageNamespace> StorageNamespaceJava::transientLocalStorageNamespace(PageGroup* pageGroup, SecurityOrigin*)
-{
-    // FIXME: A smarter implementation would create a special namespace type instead of just piggy-backing off
-    // SessionStorageNamespace here.
-    return StorageNamespaceJava::sessionStorageNamespace(*pageGroup->pages().begin());
-}
-
-StorageNamespaceJava::StorageNamespaceJava(StorageType storageType, const String& path, unsigned quota)
-    : m_storageType(storageType)
-    , m_path(path.isolatedCopy())
-//    , m_syncManager(0)
-    , m_quota(quota)
-    , m_isShutdown(false)
-{
-//    if (m_storageType == LocalStorage && !m_path.isEmpty())
-//        m_syncManager = StorageSyncManager::create(m_path);
-}
-
-StorageNamespaceJava::~StorageNamespaceJava()
-{
-    ASSERT(isMainThread());
-
-    if (m_storageType == LocalStorage) {
-        ASSERT(localStorageNamespaceMap().get(m_path) == this);
-        localStorageNamespaceMap().remove(m_path);
-    }
-
-    if (!m_isShutdown)
-        close();
-}
-
-PassRefPtr<StorageNamespace> StorageNamespaceJava::copy(Page*)
-{
-    ASSERT(isMainThread());
-    ASSERT(!m_isShutdown);
-    ASSERT(m_storageType == SessionStorage);
-
-    RefPtr<StorageNamespaceJava> newNamespace = adoptRef(new StorageNamespaceJava(m_storageType, m_path, m_quota));
-
-    StorageAreaMap::iterator end = m_storageAreaMap.end();
-    for (StorageAreaMap::iterator i = m_storageAreaMap.begin(); i != end; ++i)
-        newNamespace->m_storageAreaMap.set(i->key, i->value->copy());
-    return newNamespace.release();
-}
-
-PassRefPtr<StorageArea> StorageNamespaceJava::storageArea(PassRefPtr<SecurityOrigin> prpOrigin)
-{
-    ASSERT(isMainThread());
-    ASSERT(!m_isShutdown);
-
-    RefPtr<SecurityOrigin> origin = prpOrigin;
-    RefPtr<StorageAreaJava> storageArea;
-    if ((storageArea = m_storageAreaMap.get(origin)))
-        return storageArea.release();
-
-    storageArea = StorageAreaJava::create(m_storageType, origin, /*m_syncManager,*/ m_quota);
-    m_storageAreaMap.set(origin.release(), storageArea);
-    return storageArea.release();
-}
-
-void StorageNamespaceJava::close()
-{
-    ASSERT(isMainThread());
-
-    if (m_isShutdown)
-        return;
-
-    // If we're session storage, we shouldn't need to do any work here.
-    if (m_storageType == SessionStorage) {
-//        ASSERT(!m_syncManager);
-        return;
-    }
-
-    StorageAreaMap::iterator end = m_storageAreaMap.end();
-    for (StorageAreaMap::iterator it = m_storageAreaMap.begin(); it != end; ++it)
-        it->value->close();
-
-//    if (m_syncManager)
-//        m_syncManager->close();
-
-    m_isShutdown = true;
-}
-
-void StorageNamespaceJava::clearOriginForDeletion(SecurityOrigin* origin)
-{
-    ASSERT(isMainThread());
-
-    RefPtr<StorageAreaJava> storageArea = m_storageAreaMap.get(origin);
-    if (storageArea)
-        storageArea->clearForOriginDeletion();
-}
-
-void StorageNamespaceJava::clearAllOriginsForDeletion()
-{
-    ASSERT(isMainThread());
-
-    StorageAreaMap::iterator end = m_storageAreaMap.end();
-    for (StorageAreaMap::iterator it = m_storageAreaMap.begin(); it != end; ++it)
-        it->value->clearForOriginDeletion();
-}
-    
-void StorageNamespaceJava::sync()
-{
-    ASSERT(isMainThread());
-    StorageAreaMap::iterator end = m_storageAreaMap.end();
-    for (StorageAreaMap::iterator it = m_storageAreaMap.begin(); it != end; ++it)
-        it->value->sync();
-}
-    
-void StorageNamespaceJava::closeIdleLocalStorageDatabases()
-{
-    ASSERT(isMainThread());
-    StorageAreaMap::iterator end = m_storageAreaMap.end();
-    for (StorageAreaMap::iterator it = m_storageAreaMap.begin(); it != end; ++it)
-        it->value->closeDatabaseIfIdle();
-}
-
-} // namespace WebCore
--- a/modules/web/src/main/native/Source/WebCore/storage/java/StorageNamespaceJava.h	Wed Sep 04 12:14:42 2013 +0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/*
- * Copyright (c) 2012-2013, Oracle and/or its affiliates. All rights reserved.
- */
-
-#ifndef StorageNamespaceJava_h
-#define StorageNamespaceJava_h
-
-#include "SecurityOriginHash.h"
-#include "StorageArea.h"
-#include "StorageNamespace.h"
-
-#include <wtf/HashMap.h>
-#include <wtf/RefPtr.h>
-#include <wtf/text/WTFString.h>
-
-namespace WebCore {
-    class StorageAreaJava;
-
-    class StorageNamespaceJava : public StorageNamespace {
-    public:
-        static PassRefPtr<StorageNamespace> localStorageNamespace(PageGroup*);
-        static PassRefPtr<StorageNamespace> transientLocalStorageNamespace(PageGroup*, SecurityOrigin*);
-        static PassRefPtr<StorageNamespace> sessionStorageNamespace(Page*);
-        virtual ~StorageNamespaceJava();
-
-        virtual PassRefPtr<StorageArea> storageArea(PassRefPtr<SecurityOrigin>) OVERRIDE;
-        virtual PassRefPtr<StorageNamespace> copy(Page* newPage) OVERRIDE;
-        virtual void close() OVERRIDE;
-
-        // Not removing the origin's StorageArea from m_storageAreaMap because
-        // we're just deleting the underlying db file. If an item is added immediately
-        // after file deletion, we want the same StorageArea to eventually trigger
-        // a sync and for StorageAreaSync to recreate the backing db file.
-        virtual void clearOriginForDeletion(SecurityOrigin*) OVERRIDE;
-        virtual void clearAllOriginsForDeletion() OVERRIDE;
-        virtual void sync() OVERRIDE;
-        virtual void closeIdleLocalStorageDatabases() OVERRIDE;
-        
-    private:
-        StorageNamespaceJava(StorageType, const String& path, unsigned quota);
-
-        typedef HashMap<RefPtr<SecurityOrigin>, RefPtr<StorageAreaJava> > StorageAreaMap;
-        StorageAreaMap m_storageAreaMap;
-
-        StorageType m_storageType;
-
-        // Only used if m_storageType == LocalStorage and the path was not "" in our constructor.
-        String m_path;
-        //RefPtr<StorageSyncManager> m_syncManager;
-
-        // The default quota for each new storage area.
-        unsigned m_quota;
-
-        bool m_isShutdown;
-    };
-
-} // namespace WebCore
-
-#endif // StorageNamespaceJava_h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/test/java/javafx/scene/web/DirectoryLockTest.java	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ */
+package javafx.scene.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import static java.lang.String.format;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.AfterClass;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class DirectoryLockTest {
+
+    private static final File FOO = new File("foo");
+    private static final File BAR = new File("bar");
+    private static final File PRE_LOCKED = new File("baz");
+    private static final File[] DIRS = new File[] {FOO, BAR, PRE_LOCKED};
+
+
+    private static RandomAccessFile preLockedRaf;
+    private static FileLock preLockedLock;
+
+
+    private final ArrayList<DirectoryLock> createdLocks = new ArrayList<>();
+
+
+    @BeforeClass
+    public static void beforeClass() throws IOException {
+        for (File dir : DIRS) {
+            dir.mkdirs();
+        }
+        File preLockedFile = new File(PRE_LOCKED, ".lock");
+        preLockedRaf = new RandomAccessFile(preLockedFile, "rw");
+        preLockedLock = preLockedRaf.getChannel().tryLock();
+        if (preLockedLock == null) {
+            fail(format("Directory [%s] is already locked "
+                    + "externally", PRE_LOCKED));
+        }
+    }
+
+    @AfterClass
+    public static void afterClass() throws IOException {
+        preLockedLock.release();
+        preLockedRaf.close();
+        for (File dir : DIRS) {
+            deleteRecursively(dir);
+        }
+    }
+
+
+    @After
+    public void after() {
+        for (DirectoryLock lock : createdLocks) {
+            lock.close();
+        }
+    }
+
+
+    @Test
+    public void testConstructor() throws Exception {
+        createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        DirectoryLock lock = createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        lock.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+    }
+
+    @Test
+    public void testLockDirectoryTwice() throws Exception {
+        createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        createLock(FOO);
+        assertEquals(2, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+    }
+
+    @Test
+    public void testUnlockDirectoryTwice() throws Exception {
+        DirectoryLock lock1 = createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        DirectoryLock lock2 = createLock(FOO);
+        assertEquals(2, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        lock1.close();
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        lock2.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+    }
+
+    @Test
+    public void testLockTwoDirectoriesTwice() throws Exception {
+        createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        createLock(BAR);
+        assertEquals(1, DirectoryLock.referenceCount(BAR));
+        assertLocked(BAR);
+        createLock(FOO);
+        assertEquals(2, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        createLock(BAR);
+        assertEquals(2, DirectoryLock.referenceCount(BAR));
+        assertLocked(BAR);
+    }
+
+    @Test
+    public void testUnlockTwoDirectoriesTwice() throws Exception {
+        DirectoryLock lock1 = createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        DirectoryLock lock2 = createLock(BAR);
+        assertEquals(1, DirectoryLock.referenceCount(BAR));
+        assertLocked(BAR);
+        DirectoryLock lock3 = createLock(FOO);
+        assertEquals(2, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        DirectoryLock lock4 = createLock(BAR);
+        assertEquals(2, DirectoryLock.referenceCount(BAR));
+        assertLocked(BAR);
+        lock1.close();
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        lock2.close();
+        assertEquals(1, DirectoryLock.referenceCount(BAR));
+        assertLocked(BAR);
+        lock3.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+        lock4.close();
+        assertEquals(0, DirectoryLock.referenceCount(BAR));
+        assertNotLocked(BAR);
+    }
+
+    @Test
+    public void testConstructorNullPointerException() throws Exception {
+        try {
+            createLock(null);
+            fail("NullPointerException expected but not thrown");
+        } catch (NullPointerException expected) {}
+    }
+
+    @Test
+    public void testCanonicalization() throws Exception {
+        createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        createLock(new File(FOO.getPath() + File.separatorChar));
+        assertEquals(2, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        createLock(FOO.getCanonicalFile());
+        assertEquals(3, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+    }
+
+    public void testLockLockedDirectory() throws IOException {
+        try {
+            createLock(PRE_LOCKED);
+            fail("DirectoryAlreadyInUseException expected but not thrown");
+        } catch (DirectoryLock.DirectoryAlreadyInUseException expected) {}
+    }
+
+    @Test
+    public void testCloseClosedLock() throws Exception {
+        DirectoryLock lock = createLock(FOO);
+        assertEquals(1, DirectoryLock.referenceCount(FOO));
+        assertLocked(FOO);
+        lock.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+        lock.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+        lock.close();
+        assertEquals(0, DirectoryLock.referenceCount(FOO));
+        assertNotLocked(FOO);
+    }
+
+
+    private DirectoryLock createLock(File directory)
+        throws IOException, DirectoryLock.DirectoryAlreadyInUseException
+    {
+        DirectoryLock lock = new DirectoryLock(directory);
+        createdLocks.add(lock);
+        return lock;
+    }
+
+    private static void deleteRecursively(File file) throws IOException {
+        if (file.isDirectory()) {
+            for (File f : file.listFiles()) {
+                deleteRecursively(f);
+            }
+        }
+        if (!file.delete()) {
+            throw new IOException(String.format("Error deleting [%s]", file));
+        }
+    }
+
+    private void assertLocked(File directory) throws IOException {
+        File file = new File(directory, ".lock");
+        RandomAccessFile raf = new RandomAccessFile(file, "rw");
+        FileLock fileLock = null;
+        try {
+            fileLock = raf.getChannel().tryLock();
+            if (fileLock == null) {
+                fail(format("Directory [%s] is locked externally", directory));
+            } else {
+                fail(format("Directory [%s] is not locked", directory));
+            }
+        } catch (OverlappingFileLockException expected) {
+        } finally {
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException ignore) {}
+            }
+            try {
+                raf.close();
+            } catch (IOException ignore) {}
+        }
+    }
+
+    private void assertNotLocked(File directory) throws IOException {
+        File file = new File(directory, ".lock");
+        RandomAccessFile raf = new RandomAccessFile(file, "rw");
+        FileLock fileLock = null;
+        try {
+            fileLock = raf.getChannel().tryLock();
+            if (fileLock == null) {
+                fail(format("Directory [%s] is locked externally", directory));
+            }
+        } catch (OverlappingFileLockException ex) {
+            fail(format("Directory [%s] is locked", directory));
+        } finally {
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException ignore) {}
+            }
+            try {
+                raf.close();
+            } catch (IOException ignore) {}
+        }
+    }
+}
--- a/modules/web/src/test/java/javafx/scene/web/LoadNotificationsTest.java	Wed Sep 04 12:14:42 2013 +0400
+++ b/modules/web/src/test/java/javafx/scene/web/LoadNotificationsTest.java	Wed Sep 04 13:53:41 2013 +0400
@@ -43,6 +43,11 @@
     private void testUrl(String url) {
         log.clear();
         assertion = null;
+        submit(new Runnable() {
+            @Override public void run() {
+                checkState(RUNNING, getEngine().getLoadWorker().getState());
+            }
+        });
         load(url);
         check();
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web/src/test/java/javafx/scene/web/UserDataDirectoryTest.java	Wed Sep 04 13:53:41 2013 +0400
@@ -0,0 +1,702 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ */
+package javafx.scene.web;
+
+import java.io.File;
+import java.io.FilePermission;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import static java.lang.String.format;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.security.Permission;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import static javafx.scene.web.WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE;
+import static javafx.scene.web.WebErrorEvent.USER_DATA_DIRECTORY_IO_ERROR;
+import static javafx.scene.web.WebErrorEvent.USER_DATA_DIRECTORY_SECURITY_ERROR;
+import org.junit.After;
+import org.junit.AfterClass;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class UserDataDirectoryTest extends TestBase {
+
+    private static final File FOO = new File("foo");
+    private static final File BAR = new File("bar");
+    private static final File PRE_LOCKED = new File("baz");
+    private static final File[] DIRS = new File[] {FOO, BAR, PRE_LOCKED};
+
+
+    private static RandomAccessFile preLockedRaf;
+    private static FileLock preLockedLock;
+    private static final Random random = new Random();
+
+
+    private final ArrayList<WebEngine> createdWebEngines = new ArrayList<>();
+    private WebEngine webEngine;
+
+
+    @BeforeClass
+    public static void beforeClass() throws IOException {
+        for (File dir : DIRS) {
+            dir.mkdirs();
+        }
+        File preLockedFile = new File(PRE_LOCKED, ".lock");
+        preLockedRaf = new RandomAccessFile(preLockedFile, "rw");
+        preLockedLock = preLockedRaf.getChannel().tryLock();
+        if (preLockedLock == null) {
+            fail(format("Directory [%s] is already locked "
+                    + "externally", PRE_LOCKED));
+        }
+    }
+
+    @AfterClass
+    public static void afterClass() throws IOException {
+        preLockedLock.release();
+        preLockedRaf.close();
+        sleep(500); // Give WebKit some time to close SQLite files
+        for (File dir : DIRS) {
+            deleteRecursively(dir);
+        }
+    }
+
+
+    @Before
+    public void before() {
+        webEngine = createWebEngine();
+    }
+
+    @After
+    public void after() {
+        for (WebEngine webEngine : createdWebEngines) {
+            dispose(webEngine);
+        }
+    }
+
+
+    @Test
+    public void testDefaultValue() {
+        assertSame(null, webEngine.getUserDataDirectory());
+        // The default directory may be and often is locked by
+        // a left-over engine from another test
+        // assertLocked(defaultDirectory());
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(null, webEngine.getUserDataDirectory());
+        assertLocked(defaultDirectory());
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testSimpleModification() {
+        assertSame(null, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testSetNullValue() {
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        webEngine.setUserDataDirectory(null);
+        assertSame(null, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(null, webEngine.getUserDataDirectory());
+        assertLocked(defaultDirectory());
+        assertNotLocked(FOO);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testMultipleEnginesSharingOneDirectory() {
+        ArrayList<WebEngine> webEngines = new ArrayList<>();
+        assertNotLocked(FOO);
+        for (int i = 0; i < 5; i++) {
+            WebEngine webEngine = createWebEngine();
+            webEngines.add(webEngine);
+            webEngine.setUserDataDirectory(FOO);
+            assertSame(FOO, webEngine.getUserDataDirectory());
+            assertNotLocked(FOO);
+        }
+        for (WebEngine webEngine : webEngines) {
+            load(webEngine, new File("src/test/resources/html/ipsum.html"));
+            assertLocked(FOO);
+        }
+        assertHaveSharedLocalStorage(webEngines);
+        for (WebEngine webEngine : webEngines) {
+            assertLocked(FOO);
+            dispose(webEngine);
+        }
+        assertNotLocked(FOO);
+    }
+
+    @Test
+    public void testMultipleEnginesSharingTwoDirectories() {
+        ArrayList<WebEngine> webEnginesA = new ArrayList<>();
+        ArrayList<WebEngine> webEnginesB = new ArrayList<>();
+        assertNotLocked(FOO);
+        assertNotLocked(BAR);
+        for (int i = 0; i < 5; i++) {
+            WebEngine webEngineA = createWebEngine();
+            webEnginesA.add(webEngineA);
+            webEngineA.setUserDataDirectory(FOO);
+            assertSame(FOO, webEngineA.getUserDataDirectory());
+            assertNotLocked(FOO);
+
+            WebEngine webEngineB = createWebEngine();
+            webEnginesB.add(webEngineB);
+            webEngineB.setUserDataDirectory(BAR);
+            assertSame(BAR, webEngineB.getUserDataDirectory());
+            assertNotLocked(BAR);
+        }
+        for (WebEngine webEngineA : webEnginesA) {
+            load(webEngineA, new File("src/test/resources/html/ipsum.html"));
+            assertLocked(FOO);
+        }
+        assertHaveSharedLocalStorage(webEnginesA);
+        for (WebEngine webEngineB : webEnginesB) {
+            load(webEngineB, new File("src/test/resources/html/ipsum.html"));
+            assertLocked(BAR);
+        }
+        assertHaveSharedLocalStorage(webEnginesB);
+        for (WebEngine webEngineA : webEnginesA) {
+            assertLocked(FOO);
+            dispose(webEngineA);
+        }
+        assertNotLocked(FOO);
+        for (WebEngine webEngineB : webEnginesB) {
+            assertLocked(BAR);
+            dispose(webEngineB);
+        }
+        assertNotLocked(BAR);
+    }
+
+    @Test
+    public void testLoadEffect() {
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertNotLocked(BAR);
+        load(webEngine, new File("src/test/resources/html/h1.html"));
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        assertHasLocalStorage(webEngine);
+
+        webEngine.setUserDataDirectory(BAR);
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testLoadContent1Effect() {
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertNotLocked(BAR);
+        loadContent(webEngine, "<html/>");
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+
+        webEngine.setUserDataDirectory(BAR);
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testLoadContent2Effect() {
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertNotLocked(BAR);
+        loadContent(webEngine, "<html/>", "text/plain");
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+
+        webEngine.setUserDataDirectory(BAR);
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testExecuteScriptEffect() {
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertNotLocked(BAR);
+        submit(new Runnable() {@Override public void run() {
+            webEngine.executeScript("alert()");
+        }});
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+
+        webEngine.setUserDataDirectory(BAR);
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(FOO);
+        assertNotLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testAlreadyInUseError() {
+        webEngine.setUserDataDirectory(PRE_LOCKED);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(PRE_LOCKED, webEngine.getUserDataDirectory());
+        assertLocked(PRE_LOCKED);
+        assertHasNoLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testAlreadyInUseErrorWithPassiveHandler() {
+        webEngine.setUserDataDirectory(PRE_LOCKED);
+        ErrorHandler handler = new ErrorHandler();
+        webEngine.setOnError(handler);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(PRE_LOCKED, webEngine.getUserDataDirectory());
+        assertLocked(PRE_LOCKED);
+        assertHasNoLocalStorage(webEngine);
+        assertOccurred(USER_DATA_DIRECTORY_ALREADY_IN_USE, handler);
+    }
+
+    @Test
+    public void testAlreadyInUseErrorWithRecoveringHandler() {
+        webEngine.setUserDataDirectory(PRE_LOCKED);
+        EventHandler<WebErrorEvent> h = new EventHandler<WebErrorEvent>() {
+            @Override public void handle(WebErrorEvent event) {
+                webEngine.setUserDataDirectory(BAR);
+            }
+        };
+        webEngine.setOnError(h);
+        assertSame(PRE_LOCKED, webEngine.getUserDataDirectory());
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testIOError() {
+        String osName = System.getProperty("os.name");
+        File dir = new File(osName.toLowerCase().contains("windows")
+                ? "C:\\Windows\\foo" : "/foo");
+        webEngine.setUserDataDirectory(dir);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(dir, webEngine.getUserDataDirectory());
+        assertHasNoLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testIOErrorWithPassiveHandler() {
+        String osName = System.getProperty("os.name");
+        File dir = new File(osName.toLowerCase().contains("windows")
+                ? "C:\\Windows\\foo" : "/foo");
+        webEngine.setUserDataDirectory(dir);
+        ErrorHandler handler = new ErrorHandler();
+        webEngine.setOnError(handler);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(dir, webEngine.getUserDataDirectory());
+        assertHasNoLocalStorage(webEngine);
+        assertOccurred(USER_DATA_DIRECTORY_IO_ERROR, handler);
+    }
+
+    @Test
+    public void testIOErrorWithRecoveringHandler() {
+        String osName = System.getProperty("os.name");
+        File dir = new File(osName.toLowerCase().contains("windows")
+                ? "C:\\Windows\\foo" : "/foo");
+        webEngine.setUserDataDirectory(dir);
+        EventHandler<WebErrorEvent> h = new EventHandler<WebErrorEvent>() {
+            @Override public void handle(WebErrorEvent event) {
+                webEngine.setUserDataDirectory(BAR);
+            }
+        };
+        webEngine.setOnError(h);
+        assertSame(dir, webEngine.getUserDataDirectory());
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testSecurityError() {
+        String url = new File("src/test/resources/html/ipsum.html")
+                .toURI().toASCIIString();
+        SecurityManager oldSecurityManager = System.getSecurityManager();
+        System.setSecurityManager(new CustomSecurityManager(FOO));
+        try {
+            webEngine.setUserDataDirectory(FOO);
+            load(webEngine, url);
+        } finally {
+            System.setSecurityManager(oldSecurityManager);
+        }
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertHasNoLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testSecurityErrorWithPassiveHandler() {
+        String url = new File("src/test/resources/html/ipsum.html")
+                .toURI().toASCIIString();
+        SecurityManager oldSecurityManager = System.getSecurityManager();
+        System.setSecurityManager(new CustomSecurityManager(FOO));
+        ErrorHandler handler = new ErrorHandler();
+        try {
+            webEngine.setUserDataDirectory(FOO);
+            webEngine.setOnError(handler);
+            load(webEngine, url);
+        } finally {
+            System.setSecurityManager(oldSecurityManager);
+        }
+        assertSame(FOO, webEngine.getUserDataDirectory());
+        assertNotLocked(FOO);
+        assertHasNoLocalStorage(webEngine);
+        assertOccurred(USER_DATA_DIRECTORY_SECURITY_ERROR, handler);
+    }
+
+    @Test
+    public void testSecurityErrorWithRecoveringHandler() {
+        String url = new File("src/test/resources/html/ipsum.html")
+                .toURI().toASCIIString();
+        SecurityManager oldSecurityManager = System.getSecurityManager();
+        System.setSecurityManager(new CustomSecurityManager(FOO));
+        try {
+            webEngine.setUserDataDirectory(FOO);
+            EventHandler<WebErrorEvent> h = new EventHandler<WebErrorEvent>() {
+                @Override public void handle(WebErrorEvent event) {
+                    webEngine.setUserDataDirectory(BAR);
+                }
+            };
+            webEngine.setOnError(h);
+            assertSame(FOO, webEngine.getUserDataDirectory());
+            load(webEngine, url);
+        } finally {
+            System.setSecurityManager(oldSecurityManager);
+        }
+        assertSame(BAR, webEngine.getUserDataDirectory());
+        assertLocked(BAR);
+        assertHasLocalStorage(webEngine);
+    }
+
+    @Test
+    public void testDisposal() {
+        webEngine.setUserDataDirectory(FOO);
+        load(webEngine, new File("src/test/resources/html/ipsum.html"));
+        assertLocked(FOO);
+        dispose(webEngine);
+        assertNotLocked(FOO);
+    }
+
+    @Test
+    public void testPropertyObjectSanity() {
+        ObjectProperty<File> property = webEngine.userDataDirectoryProperty();
+        assertNotNull(property);
+        assertSame(property, webEngine.userDataDirectoryProperty());
+        assertSame(null, property.get());
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(FOO, property.get());
+    }
+
+    @Test
+    public void testPropertyBinding() {
+        ObjectProperty<File> otherProperty = new SimpleObjectProperty<>();
+        otherProperty.bind(webEngine.userDataDirectoryProperty());
+        assertSame(webEngine.getUserDataDirectory(), otherProperty.get());
+        webEngine.setUserDataDirectory(FOO);
+        assertSame(webEngine.getUserDataDirectory(), otherProperty.get());
+    }
+
+    @Test
+    public void testNoFxThreadCheck() throws IOException {
+        webEngine.getUserDataDirectory();
+        webEngine.setUserDataDirectory(FOO);
+    }
+
+
+    private WebEngine createWebEngine() {
+        WebEngine result;
+        if (Platform.isFxApplicationThread()) {
+            result = new WebEngine();
+        } else {
+            result = submit(new Callable<WebEngine>() {
+                @Override public WebEngine call() {
+                    return new WebEngine();
+                }
+            });
+        }
+        createdWebEngines.add(result);
+        return result;
+    }
+
+    private void load(WebEngine webEngine, File file) {
+        load(webEngine, file.toURI().toASCIIString());
+    }
+
+    private void load(final WebEngine webEngine, final String url) {
+        executeLoadJob(webEngine, new Runnable() {@Override public void run() {
+            webEngine.load(url);
+        }});
+    }
+
+    private void loadContent(final WebEngine webEngine, final String content) {
+        executeLoadJob(webEngine, new Runnable() {@Override public void run() {
+            webEngine.loadContent(content);
+        }});
+    }
+
+    private void loadContent(final WebEngine webEngine, final String content,
+                             final String contentType)
+    {
+        executeLoadJob(webEngine, new Runnable() {@Override public void run() {
+            webEngine.loadContent(content, contentType);
+        }});
+    }
+
+    private void executeLoadJob(final WebEngine webEngine, final Runnable job) {
+        final CountDownLatch latch = new CountDownLatch(1);
+        submit(new Runnable() {@Override public void run() {
+            webEngine.getLoadWorker().runningProperty().addListener(
+                    new ChangeListener<Boolean>() {
+                        @Override public void changed(
+                                ObservableValue<? extends Boolean> ov,
+                                Boolean oldValue, Boolean newValue)
+                        {
+                            if (!newValue) {
+                                latch.countDown();
+                            }
+                        }
+                    });
+            job.run();
+        }});
+        try {
+            latch.await();
+        } catch (InterruptedException ex) {
+            throw new AssertionError(ex);
+        }
+    }
+
+    private void dispose(final WebEngine webEngine) {
+        Runnable runnable = new Runnable() {@Override public void run() {
+            webEngine.dispose();
+        }};
+        if (Platform.isFxApplicationThread()) {
+            runnable.run();
+        } else {
+            submit(runnable);
+        }
+    }
+
+    private static void deleteRecursively(File file) throws IOException {
+        if (file.isDirectory()) {
+            for (File f : file.listFiles()) {
+                deleteRecursively(f);
+            }
+        }
+        if (!file.delete()) {
+            throw new IOException(String.format("Error deleting [%s]", file));
+        }
+    }
+
+    private void assertLocked(File directory) {
+        File file = new File(directory, ".lock");
+        RandomAccessFile raf = null;
+        FileLock fileLock = null;
+        try {
+            raf = new RandomAccessFile(file, "rw");
+            fileLock = raf.getChannel().tryLock();
+            if (fileLock == null) {
+                fail(format("Directory [%s] is locked externally", directory));
+            } else {
+                fail(format("Directory [%s] is not locked", directory));
+            }
+        } catch (OverlappingFileLockException expected) {
+        } catch (IOException ex) {
+            throw new AssertionError(ex);
+        } finally {
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException ignore) {}
+            }
+            if (raf != null) {
+                try {
+                    raf.close();
+                } catch (IOException ignore) {}
+            }
+        }
+    }
+
+    private void assertNotLocked(File directory) {
+        File file = new File(directory, ".lock");
+        RandomAccessFile raf = null;
+        FileLock fileLock = null;
+        try {
+            raf = new RandomAccessFile(file, "rw");
+            fileLock = raf.getChannel().tryLock();
+            if (fileLock == null) {
+                fail(format("Directory [%s] is locked externally", directory));
+            }
+        } catch (OverlappingFileLockException ex) {
+            fail(format("Directory [%s] is locked", directory));
+        } catch (IOException ex) {
+            throw new AssertionError(ex);
+        } finally {
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException ignore) {}
+            }
+            if (raf != null) {
+                try {
+                    raf.close();
+                } catch (IOException ignore) {}
+            }
+        }
+    }
+
+    private void assertHasLocalStorage(WebEngine webEngine) {
+        assertHaveSharedLocalStorage(Arrays.asList(webEngine));
+    }
+
+    private void assertHaveSharedLocalStorage(
+            final Collection<WebEngine> webEngines)
+    {
+        Runnable runnable = new Runnable() {@Override public void run() {
+            for (WebEngine webEngine : webEngines) {
+                assertNotNull(webEngine.executeScript("localStorage"));
+            }
+            for (WebEngine webEngine : webEngines) {
+                String key = "key" + random.nextInt();
+                String value1 = "value" + random.nextInt();
+                webEngine.executeScript("localStorage.setItem('"
+                        + key + "', '" + value1 + "')");
+                for (WebEngine otherWebEngine : webEngines) {
+                    String value2 = (String) otherWebEngine.executeScript(
+                            "localStorage.getItem('" + key + "')");
+                    assertEquals(value1, value2);
+                }
+            }
+        }};
+        if (Platform.isFxApplicationThread()) {
+            runnable.run();
+        } else {
+            submit(runnable);
+        }
+    }
+
+    private void assertHasNoLocalStorage(final WebEngine webEngine) {
+        Runnable runnable = new Runnable() {@Override public void run() {
+            assertNull(webEngine.executeScript("localStorage"));
+        }};
+        if (Platform.isFxApplicationThread()) {
+            runnable.run();
+        } else {
+            submit(runnable);
+        }
+    }
+
+    private static void assertOccurred(EventType<WebErrorEvent> eventType,
+                                       ErrorHandler handler)
+    {
+        assertEquals(1, handler.errors.size());
+        assertSame(eventType, handler.errors.get(0).getEventType());
+    }
+
+    private static File defaultDirectory() {
+        return new File(
+                com.sun.glass.ui.Application.GetApplication()
+                        .getDataDirectory(),
+                "webview");
+    }
+
+    private static void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException ex) {
+            throw new AssertionError(ex);
+        }
+    }
+
+    private static final class ErrorHandler
+        implements EventHandler<WebErrorEvent>
+    {
+        private final ArrayList<WebErrorEvent> errors = new ArrayList<>();
+
+        @Override public void handle(WebErrorEvent event) {
+            errors.add(event);
+            System.err.println("onError: " + event);
+        }
+    }
+
+    private static final class CustomSecurityManager extends SecurityManager {
+        private final String path;
+
+        private CustomSecurityManager(File path) {
+            try {
+                this.path = path.getCanonicalPath();
+            } catch (IOException ex) {
+                throw new AssertionError(ex);
+            }
+        }
+
+        @Override public void checkPermission(Permission perm) {
+            if (perm instanceof FilePermission
+                    && perm.getName().startsWith(path))
+            {
+                super.checkPermission(perm);
+            }
+        }
+    }
+}