Repository: firebase/firebase-admin-java
Pull request #299 (2019-08-12 20:52:59)

feat(rtdb): Connect to RTDB emulator when valid emulator URL is passed OR env vars are set correctly
This PR makes it possible for the Java admin SDK to talk to the RTDB emulator instead of the actual database instance.
This can happen in the following fashion:-
1. If the user passes in a valid emulator URL (http scheme only, host can't contain `firebaseio.com`, has the `ns` query parameter)
2. Else, if the environment variable `FIREBASE_RTDB_EMULATOR_HOST` is set to a value of the form `host:port`.

If either 1. or 2. is true, we decide to talk to the RTDB emulator. In such cases, we 
- provide a valid emulator URL (either by replacing the supplied URL or making a new one)
- replace existing credentials with `EmulatorCredentials`, which use `owner` as the access token.

URL replacement happens in three places:- 
1. while constructing `FirebaseOptions` instances in `FirebaseApp`
2. when a specific `databaseUrl` is passed in `FirebaseDatabase.getInstance(String url)`
3. when a specific URL with path is passed in `FirebaseDatabase.getReferenceFromUrl(String url)`


Tests have been added with different combinations of passed-in URLs and environment variable values for classes `FirebaseApp`, `FirebaseDatabase`, and also for  `EmulatorHelper`: the helper class that actually does URL replacement if necessary.

**Notes for reviewers**
I have made changes to existing behavior to `Utilities.parseUrl(String url)`: these changes were necessary to recognize the `ns` query parameter, and also to suppost valid `<net_loc>` URL semantics where a URL like `http://localhost:8080/` is deemed valid.
These changes are nearly identical to changes to the same file in [this Android SDK PR](https://github.com/firebase/firebase-android-sdk/pull/680/files).



Patch:
diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java
index c7f2447..f69ee36 100644
--- a/src/main/java/com/google/firebase/FirebaseApp.java
+++ b/src/main/java/com/google/firebase/FirebaseApp.java
@@ -19,6 +19,7 @@ package com.google.firebase;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.firebase.FirebaseOptions.APPLICATION_DEFAULT_CREDENTIALS;
 
 import com.google.api.client.googleapis.util.Utils;
 import com.google.api.client.json.JsonFactory;
@@ -33,6 +34,7 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.firebase.internal.FirebaseAppStore;
 import com.google.firebase.internal.FirebaseScheduledExecutor;
@@ -582,10 +584,9 @@ public class FirebaseApp {
     String defaultConfig = System.getenv(FIREBASE_CONFIG_ENV_VAR);
     if (Strings.isNullOrEmpty(defaultConfig)) {
       return new FirebaseOptions.Builder()
-        .setCredentials(GoogleCredentials.getApplicationDefault())
-        .build();
+          .setCredentials(APPLICATION_DEFAULT_CREDENTIALS)
+          .build();
     }
-
     JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
     FirebaseOptions.Builder builder = new FirebaseOptions.Builder();
     JsonParser parser;
@@ -597,7 +598,7 @@ public class FirebaseApp {
       parser = jsonFactory.createJsonParser(reader);
     }
     parser.parseAndClose(builder);
-    builder.setCredentials(GoogleCredentials.getApplicationDefault());
+    builder.setCredentials(APPLICATION_DEFAULT_CREDENTIALS);
     return builder.build();
   }
 }
diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java
index 49c068e..b93d92f 100644
--- a/src/main/java/com/google/firebase/FirebaseOptions.java
+++ b/src/main/java/com/google/firebase/FirebaseOptions.java
@@ -26,11 +26,14 @@ import com.google.api.client.util.Key;
 import com.google.auth.oauth2.GoogleCredentials;
 import com.google.cloud.firestore.FirestoreOptions;
 import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.firebase.internal.FirebaseThreadManagers;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -56,9 +59,21 @@ public final class FirebaseOptions {
           "https://www.googleapis.com/auth/cloud-platform",
           "https://www.googleapis.com/auth/datastore");
 
+  static final Supplier<GoogleCredentials> APPLICATION_DEFAULT_CREDENTIALS =
+      new Supplier<GoogleCredentials>() {
+        @Override
+        public GoogleCredentials get() {
+          try {
+            return GoogleCredentials.getApplicationDefault().createScoped(FIREBASE_SCOPES);
+          } catch (IOException e) {
+            throw new IllegalStateException(e);
+          }
+        }
+      };
+
   private final String databaseUrl;
   private final String storageBucket;
-  private final GoogleCredentials credentials;
+  private final Supplier<GoogleCredentials> credentialsSupplier;
   private final Map<String, Object> databaseAuthVariableOverride;
   private final String projectId;
   private final String serviceAccountId;
@@ -69,11 +84,10 @@ public final class FirebaseOptions {
   private final ThreadManager threadManager;
   private final FirestoreOptions firestoreOptions;
 
-  private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
-    this.credentials = checkNotNull(builder.credentials,
-        "FirebaseOptions must be initialized with setCredentials().")
-        .createScoped(FIREBASE_SCOPES);
+  private FirebaseOptions(@NonNull final FirebaseOptions.Builder builder) {
     this.databaseUrl = builder.databaseUrl;
+    this.credentialsSupplier = checkNotNull(
+        builder.credentialsSupplier, "FirebaseOptions must be initialized with setCredentials().");
     this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride;
     this.projectId = builder.projectId;
     if (!Strings.isNullOrEmpty(builder.storageBucket)) {
@@ -118,7 +132,7 @@ public final class FirebaseOptions {
   }
 
   GoogleCredentials getCredentials() {
-    return credentials;
+    return credentialsSupplier.get();
   }
 
   /**
@@ -227,8 +241,7 @@ public final class FirebaseOptions {
 
     @Key("serviceAccountId")
     private String serviceAccountId;
-    
-    private GoogleCredentials credentials;
+    private Supplier<GoogleCredentials> credentialsSupplier;
     private FirestoreOptions firestoreOptions;
     private HttpTransport httpTransport = Utils.getDefaultTransport();
     private JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
@@ -248,7 +261,7 @@ public final class FirebaseOptions {
     public Builder(FirebaseOptions options) {
       databaseUrl = options.databaseUrl;
       storageBucket = options.storageBucket;
-      credentials = options.credentials;
+      credentialsSupplier = options.credentialsSupplier;
       databaseAuthVariableOverride = options.databaseAuthVariableOverride;
       projectId = options.projectId;
       httpTransport = options.httpTransport;
@@ -308,7 +321,20 @@ public final class FirebaseOptions {
      * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
      */
     public Builder setCredentials(GoogleCredentials credentials) {
-      this.credentials = checkNotNull(credentials);
+      this.credentialsSupplier = Suppliers
+          .ofInstance(checkNotNull(credentials).createScoped(FIREBASE_SCOPES));
+      return this;
+    }
+
+    /**
+     * Sets the <code>Supplier</code> of <code>GoogleCredentials</code> to use to authenticate the
+     * SDK. This is NOT intended for public use outside the SDK.
+     *
+     * @param credentialsSupplier Supplier instance that wraps GoogleCredentials.
+     * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
+     */
+    Builder setCredentials(Supplier<GoogleCredentials> credentialsSupplier) {
+      this.credentialsSupplier = checkNotNull(credentialsSupplier);
       return this;
     }
 
diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
index 2ddb55d..0a79932 100644
--- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java
+++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
@@ -19,6 +19,10 @@ package com.google.firebase.database;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.ImplFirebaseTrampolines;
@@ -27,15 +31,20 @@ import com.google.firebase.database.core.Path;
 import com.google.firebase.database.core.Repo;
 import com.google.firebase.database.core.RepoInfo;
 import com.google.firebase.database.core.RepoManager;
+import com.google.firebase.database.util.EmulatorHelper;
 import com.google.firebase.database.utilities.ParsedUrl;
 import com.google.firebase.database.utilities.Utilities;
 import com.google.firebase.database.utilities.Validation;
 import com.google.firebase.internal.FirebaseService;
 
 import com.google.firebase.internal.SdkUtils;
+import java.io.IOException;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -112,15 +121,21 @@ public class FirebaseDatabase {
     if (service == null) {
       service = ImplFirebaseTrampolines.addService(app, new FirebaseDatabaseService());
     }
-
-    DatabaseInstances dbInstances = service.getInstance();
     if (url == null || url.isEmpty()) {
       throw new DatabaseException(
           "Failed to get FirebaseDatabase instance: Specify DatabaseURL within "
               + "FirebaseApp or from your getInstance() call.");
     }
-
-    ParsedUrl parsedUrl = Utilities.parseUrl(url);
+    ParsedUrl parsedUrl;
+    boolean connectingToEmulator = false;
+    String possibleEmulatorUrl = EmulatorHelper
+        .getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv());
+    if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) {
+      parsedUrl = Utilities.parseUrl(possibleEmulatorUrl);
+      connectingToEmulator = true;
+    } else {
+      parsedUrl = Utilities.parseUrl(url);
+    }
     if (!parsedUrl.path.isEmpty()) {
       throw new DatabaseException(
           "Specified Database URL '"
@@ -130,6 +145,7 @@ public class FirebaseDatabase {
               + parsedUrl.path.toString());
     }
 
+    DatabaseInstances dbInstances = service.getInstance();
     FirebaseDatabase database = dbInstances.get(parsedUrl.repoInfo);
     if (database == null) {
       DatabaseConfig config = new DatabaseConfig();
@@ -140,11 +156,12 @@ public class FirebaseDatabase {
         config.setSessionPersistenceKey(app.getName());
       }
       config.setFirebaseApp(app);
-
+      if (connectingToEmulator) {
+        config.setCustomCredentials(new EmulatorCredentials(), true);
+      }
       database = new FirebaseDatabase(app, parsedUrl.repoInfo, config);
       dbInstances.put(parsedUrl.repoInfo, database);
     }
-
     return database;
   }
 
@@ -207,6 +224,11 @@ public class FirebaseDatabase {
   public DatabaseReference getReferenceFromUrl(String url) {
     checkNotNull(url,
         "Can't pass null for argument 'url' in FirebaseDatabase.getReferenceFromUrl()");
+    String possibleEmulatorUrl = EmulatorHelper
+        .getEmulatorUrl(url, EmulatorHelper.getEmulatorHostFromEnv());
+    if (!Strings.isNullOrEmpty(possibleEmulatorUrl)) {
+      url = possibleEmulatorUrl;
+    }
     ParsedUrl parsedUrl = Utilities.parseUrl(url);
     Repo repo = ensureRepo();
     if (!parsedUrl.repoInfo.host.equals(repo.getRepoInfo().host)) {
@@ -380,4 +402,26 @@ public class FirebaseDatabase {
       instance.destroy();
     }
   }
+
+  private static class EmulatorCredentials extends GoogleCredentials {
+
+    EmulatorCredentials() {
+      super(newToken());
+    }
+
+    private static AccessToken newToken() {
+      return new AccessToken("owner",
+          new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)));
+    }
+
+    @Override
+    public AccessToken refreshAccessToken() {
+      return newToken();
+    }
+
+    @Override
+    public Map<String, List<String>> getRequestMetadata() throws IOException {
+      return ImmutableMap.of();
+    }
+  }
 }
diff --git a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java
index 9769b65..403a131 100644
--- a/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java
+++ b/src/main/java/com/google/firebase/database/connection/NettyWebSocketClient.java
@@ -35,8 +35,11 @@ import io.netty.handler.ssl.SslContextBuilder;
 import java.io.EOFException;
 import java.net.URI;
 import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadFactory;
+import javax.net.ssl.SSLException;
 import javax.net.ssl.TrustManagerFactory;
 
 /**
@@ -57,13 +60,15 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient {
   private final ChannelHandler channelHandler;
   private final ExecutorService executorService;
   private final EventLoopGroup group;
+  private final boolean isSecure;
 
   private Channel channel;
 
   NettyWebSocketClient(
-      URI uri, String userAgent, ThreadFactory threadFactory,
+      URI uri, boolean isSecure, String userAgent, ThreadFactory threadFactory,
       WebsocketConnection.WSClientEventHandler eventHandler) {
     this.uri = checkNotNull(uri, "uri must not be null");
+    this.isSecure = isSecure;
     this.eventHandler = checkNotNull(eventHandler, "event handler must not be null");
     this.channelHandler = new WebSocketClientHandler(uri, userAgent, eventHandler);
     this.executorService = new FirebaseScheduledExecutor(threadFactory,
@@ -75,20 +80,26 @@ class NettyWebSocketClient implements WebsocketConnection.WSClient {
   public void connect() {
     checkState(channel == null, "channel already initialized");
     try {
-      TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
-          TrustManagerFactory.getDefaultAlgorithm());
-      trustFactory.init((KeyStore) null);
-      final SslContext sslContext = SslContextBuilder.forClient()
-          .trustManager(trustFactory).build();
       Bootstrap bootstrap = new Bootstrap();
+      SslContext sslContext = null;
+      if (this.isSecure) {
+        TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(
+            TrustManagerFactory.getDefaultAlgorithm());
+        trustFactory.init((KeyStore) null);
+        sslContext = SslContextBuilder.forClient()
+            .trustManager(trustFactory).build();
+      }
       final int port = uri.getPort() != -1 ? uri.getPort() : DEFAULT_WSS_PORT;
+      final SslContext finalSslContext = sslContext;
       bootstrap.group(group)
           .channel(NioSocketChannel.class)
           .handler(new ChannelInitializer<SocketChannel>() {
             @Override
             protected void initChannel(SocketChannel ch) {
               ChannelPipeline p = ch.pipeline();
-              p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), port));
+              if (finalSslContext != null) {
+                p.addLast(finalSslContext.newHandler(ch.alloc(), uri.getHost(), port));
+              }
               p.addLast(
                   new HttpClientCodec(),
                   // Set the max size for the HTTP responses. This only applies to the WebSocket
diff --git a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
index 3e29db0..a421b90 100644
--- a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
+++ b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
@@ -413,8 +413,8 @@ class WebsocketConnection {
       String host = (optCachedHost != null) ? optCachedHost : hostInfo.getHost();
       URI uri = HostInfo.getConnectionUrl(
           host, hostInfo.isSecure(), hostInfo.getNamespace(), optLastSessionId);
-      return new NettyWebSocketClient(
-          uri, context.getUserAgent(), context.getThreadFactory(), delegate);
+      return new NettyWebSocketClient(uri, hostInfo.isSecure(), context.getUserAgent(),
+          context.getThreadFactory(), delegate);
     }
   }
 
diff --git a/src/main/java/com/google/firebase/database/core/Context.java b/src/main/java/com/google/firebase/database/core/Context.java
index da68861..a0025f9 100644
--- a/src/main/java/com/google/firebase/database/core/Context.java
+++ b/src/main/java/com/google/firebase/database/core/Context.java
@@ -16,6 +16,7 @@
 
 package com.google.firebase.database.core;
 
+import com.google.auth.oauth2.GoogleCredentials;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.database.DatabaseException;
@@ -233,4 +234,13 @@ public class Context {
             .append(platformAgent);
     return sb.toString();
   }
+
+  public void setCustomCredentials(GoogleCredentials customCredentials, boolean autoRefresh) {
+    // ensure that platform exists
+    getPlatform();
+    // ensure that runloop exists else we might get a NPE
+    this.ensureRunLoop();
+    this.authTokenProvider = new JvmAuthTokenProvider(firebaseApp, this.getExecutorService(),
+        autoRefresh, customCredentials);
+  }
 }
diff --git a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java
index 541a4e4..a1cb796 100644
--- a/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java
+++ b/src/main/java/com/google/firebase/database/core/JvmAuthTokenProvider.java
@@ -41,7 +41,12 @@ public class JvmAuthTokenProvider implements AuthTokenProvider {
   }
 
   JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh) {
-    this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
+    this(firebaseApp, executor, autoRefresh, ImplFirebaseTrampolines.getCredentials(firebaseApp));
+  }
+
+  JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh,
+      GoogleCredentials customCredentials) {
+    this.credentials = customCredentials;
     this.authVariable = firebaseApp.getOptions().getDatabaseAuthVariableOverride();
     this.executor = executor;
     if (autoRefresh) {
diff --git a/src/main/java/com/google/firebase/database/util/EmulatorHelper.java b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java
new file mode 100644
index 0000000..833f976
--- /dev/null
+++ b/src/main/java/com/google/firebase/database/util/EmulatorHelper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.firebase.database.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.firebase.database.annotations.Nullable;
+import com.google.firebase.database.core.RepoInfo;
+import com.google.firebase.database.utilities.ParsedUrl;
+import com.google.firebase.database.utilities.Utilities;
+
+public final class EmulatorHelper {
+
+  private EmulatorHelper() {
+  }
+
+  @VisibleForTesting
+  public static final String FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR = "FIREBASE_RTDB_EMULATOR_HOST";
+
+  public static String getEmulatorHostFromEnv() {
+    return System.getenv(FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR);
+  }
+
+  @VisibleForTesting
+  @Nullable
+  static boolean isEmulatorUrl(String databaseUrl) {
+    if (Strings.isNullOrEmpty(databaseUrl)) {
+      return false;
+    }
+    RepoInfo repoInfo = Utilities.parseUrl(databaseUrl).repoInfo;
+    return !repoInfo.host.endsWith(".firebaseio.com") && databaseUrl.contains("ns=");
+  }
+
+  @Nullable
+  public static String getEmulatorUrl(String suppliedDatabaseUrl, String emulatorHost) {
+    if (isEmulatorUrl(suppliedDatabaseUrl)) {
+      return suppliedDatabaseUrl;
+    }
+    if (Strings.isNullOrEmpty(emulatorHost)) {
+      return null;
+    }
+    if (emulatorHost.contains("http:") || emulatorHost.contains("?ns=")) {
+      throw new IllegalArgumentException(
+          "emulator host declared in environment variable must be of the format \"host:port\"");
+    }
+    String namespaceName = "default";
+    String path = "/";
+    if (!Strings.isNullOrEmpty(suppliedDatabaseUrl)) {
+      ParsedUrl parsedDbUrl = Utilities.parseUrl(suppliedDatabaseUrl);
+      namespaceName = parsedDbUrl.repoInfo.namespace;
+      path = parsedDbUrl.path.isEmpty() ? "/" : parsedDbUrl.path.toString() + "/";
+    }
+    // Must format correctly
+    return String.format("http://%s%s?ns=%s", emulatorHost, path, namespaceName);
+  }
+}
diff --git a/src/main/java/com/google/firebase/database/utilities/Utilities.java b/src/main/java/com/google/firebase/database/utilities/Utilities.java
index 88c932e..5a3c648 100644
--- a/src/main/java/com/google/firebase/database/utilities/Utilities.java
+++ b/src/main/java/com/google/firebase/database/utilities/Utilities.java
@@ -18,78 +18,107 @@ package com.google.firebase.database.utilities;
 
 import com.google.api.core.ApiFuture;
 import com.google.api.core.SettableApiFuture;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
+import com.google.common.net.UrlEscapers;
 import com.google.firebase.database.DatabaseError;
 import com.google.firebase.database.DatabaseException;
 import com.google.firebase.database.DatabaseReference;
 import com.google.firebase.database.core.Path;
 import com.google.firebase.database.core.RepoInfo;
-
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
-import java.net.URISyntaxException;
+import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Map;
+import org.apache.http.client.utils.URLEncodedUtils;
 
 public class Utilities {
-
   private static final char[] HEX_CHARACTERS = "0123456789abcdef".toCharArray();
 
   public static ParsedUrl parseUrl(String url) throws DatabaseException {
-    String original = url;
     try {
-      int schemeOffset = original.indexOf("//");
-      if (schemeOffset == -1) {
-        throw new URISyntaxException(original, "Invalid scheme specified");
-      }
-      int pathOffset = original.substring(schemeOffset + 2).indexOf("/");
-      if (pathOffset != -1) {
-        pathOffset += schemeOffset + 2;
-        String[] pathSegments = original.substring(pathOffset).split("/");
-        StringBuilder builder = new StringBuilder();
-        for (int i = 0; i < pathSegments.length; ++i) {
-          if (!pathSegments[i].equals("")) {
-            builder.append("/");
-            builder.append(URLEncoder.encode(pathSegments[i], "UTF-8"));
-          }
-        }
-        original = original.substring(0, pathOffset) + builder.toString();
-      }
+      URI uri = URI.create(url);
 
-      URI uri = new URI(original);
-      // URLEncoding a space turns it into a '+', which is different
-      // from our expected behavior. Do a manual replace to fix it.
-      final String pathString = uri.getPath().replace("+", " ");
-      Validation.validateRootPathString(pathString);
       String scheme = uri.getScheme();
+      if (scheme == null) {
+        throw new IllegalArgumentException("Database URL does not specify a URL scheme");
+      }
+
+      String host = uri.getHost();
+      if (host == null) {
+        throw new IllegalArgumentException("Database URL does not specify a valid host");
+      }
 
       RepoInfo repoInfo = new RepoInfo();
-      repoInfo.host = uri.getHost().toLowerCase();
+      repoInfo.host = host.toLowerCase();
+      repoInfo.secure = scheme.equals("https") || scheme.equals("wss");
 
       int port = uri.getPort();
       if (port != -1) {
-        repoInfo.secure = scheme.equals("https");
         repoInfo.host += ":" + port;
+      }
+
+      Map<String, String> params = getQueryParamsMap(uri.getRawQuery());
+      String namespaceParam = params.get("ns");
+      if (!Strings.isNullOrEmpty(namespaceParam)) {
+        repoInfo.namespace = namespaceParam;
       } else {
-        repoInfo.secure = true;
+        String[] parts = host.split("\\.", -1);
+        repoInfo.namespace = parts[0].toLowerCase();
       }
-      String[] parts = repoInfo.host.split("\\.");
 
-      repoInfo.namespace = parts[0].toLowerCase();
       repoInfo.internalHost = repoInfo.host;
+      // use raw (encoded) path for backwards compatibility.
+      String pathString = uri.getRawPath();
+      pathString = pathString.replace("+", " ");
+      Validation.validateRootPathString(pathString);
+
       ParsedUrl parsedUrl = new ParsedUrl();
       parsedUrl.path = new Path(pathString);
       parsedUrl.repoInfo = repoInfo;
+
       return parsedUrl;
+    } catch (Exception e) {
+      throw new DatabaseException("Invalid Firebase Database url specified: " + url, e);
+    }
+  }
 
-    } catch (URISyntaxException e) {
-      throw new DatabaseException("Invalid Firebase Database url specified", e);
-    } catch (UnsupportedEncodingException e) {
-      throw new DatabaseException("Failed to URLEncode the path", e);
+  /**
+   * Extracts a map of query parameters from an encoded query string. Repeated parameters have
+   * values concatenated with commas.
+   *
+   * @param queryString to parse params from. Must be encoded.
+   * @return map of query parameters and their values.
+   */
+  @VisibleForTesting
+  static Map<String, String> getQueryParamsMap(String queryString)
+      throws UnsupportedEncodingException {
+    Map<String, String> paramsMap = new HashMap<>();
+    if (Strings.isNullOrEmpty(queryString)) {
+      return paramsMap;
+    }
+    String[] paramPairs = queryString.split("&");
+    for (String paramPair : paramPairs) {
+      String[] pairParts = paramPair.split("=");
+      // both the first and second part will be encoded now, we must decode them
+      String decodedKey = URLDecoder.decode(pairParts[0], Charsets.UTF_8.name());
+      String decodedValue = URLDecoder.decode(pairParts[1], Charsets.UTF_8.name());
+      String runningValue = paramsMap.get(decodedKey);
+      if (Strings.isNullOrEmpty(runningValue)) {
+        runningValue = decodedValue;
+      } else {
+        runningValue += "," + decodedValue;
+      }
+      paramsMap.put(pairParts[0], runningValue);
     }
+    return paramsMap;
   }
 
   public static String[] splitIntoFrames(String src, int maxFrameSize) {


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java
index c7036b4..ed3fb06 100644
--- a/src/test/java/com/google/firebase/FirebaseAppTest.java
+++ b/src/test/java/com/google/firebase/FirebaseAppTest.java
@@ -35,6 +35,8 @@ import com.google.auth.oauth2.OAuth2Credentials;
 import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener;
 import com.google.common.base.Defaults;
 import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.firebase.FirebaseApp.TokenRefresher;
@@ -580,6 +582,13 @@ public class FirebaseAppTest {
     new FirebaseException("");
   }
 
+  @Test
+  public void testFirebaseAppCreationWithEmptySupplier() {
+    FirebaseApp.initializeApp(FirebaseOptions.builder()
+        .setDatabaseUrl("https://test-ns.firebaseio.com")
+        .setCredentials(Suppliers.<GoogleCredentials>ofInstance(null)).build());
+  }
+
   private static void setFirebaseConfigEnvironmentVariable(String configJSON) {
     String configValue;
     if (configJSON.isEmpty() || configJSON.startsWith("{")) {
diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
index 0623a38..a3dac26 100644
--- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java
+++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
@@ -156,12 +156,12 @@ public class FirebaseOptionsTest {
 
   @Test(expected = NullPointerException.class)
   public void createOptionsWithCredentialMissing() {
-    new FirebaseOptions.Builder().build();
+    new FirebaseOptions.Builder().build().getCredentials();
   }
 
   @Test(expected = NullPointerException.class)
   public void createOptionsWithNullCredentials() {
-    new FirebaseOptions.Builder().setCredentials(null).build();
+    new FirebaseOptions.Builder().setCredentials((GoogleCredentials) null).build();
   }
 
   @Test(expected = IllegalArgumentException.class)
diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java
index 6f2a3f7..74e8358 100644
--- a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java
+++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java
@@ -23,17 +23,17 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.database.util.EmulatorHelper;
 import com.google.firebase.testing.ServiceAccount;
-
 import com.google.firebase.testing.TestUtils;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
+import java.util.List;
 import org.junit.Test;
 
 public class FirebaseDatabaseTest {
@@ -44,100 +44,118 @@ public class FirebaseDatabaseTest {
           .setDatabaseUrl("https://firebase-db-test.firebaseio.com")
           .build();
 
-  @BeforeClass
-  public static void setupClass() {
-    FirebaseApp.initializeApp(firebaseOptions);
-  }
-
-  @AfterClass
-  public static void tearDownClass() {
-    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
-  }
-
   @Test
-  public void testGetInstance() throws ExecutionException, InterruptedException {
-    FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
-    assertNotNull(defaultDatabase);
-    assertSame(defaultDatabase, FirebaseDatabase.getInstance());
-    assertSame(FirebaseApp.getInstance(), defaultDatabase.getApp());
+  public void testGetInstance() {
+    FirebaseApp.initializeApp(firebaseOptions);
+    try {
+      FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
+      assertNotNull(defaultDatabase);
+      assertSame(defaultDatabase, FirebaseDatabase.getInstance());
+      assertSame(FirebaseApp.getInstance(), defaultDatabase.getApp());
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
   }
 
   @Test
-  public void testGetInstanceForUrl() throws ExecutionException, InterruptedException {
-    String url = "https://firebase-db-test2.firebaseio.com";
-    FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(url);
-    assertNotNull(otherDatabase);
-    assertNotSame(otherDatabase, FirebaseDatabase.getInstance());
+  public void testGetInstanceForUrl() {
+    FirebaseApp.initializeApp(firebaseOptions);
+    try {
+      String url = "https://firebase-db-test2.firebaseio.com";
+      FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(url);
+      assertNotNull(otherDatabase);
+      assertNotSame(otherDatabase, FirebaseDatabase.getInstance());
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
   }
 
   @Test
-  public void testInvalidUrl() throws ExecutionException, InterruptedException {
-    String[] urls = new String[]{
-        null, "", "https://firebase-db-test.firebaseio.com/foo"
-    };
-    for (String url : urls) {
-      try {
-        FirebaseDatabase.getInstance(url);
-        fail("No error thrown for URL: " + url);
-      } catch (DatabaseException expected) {
-        // expected
+  public void testInvalidUrl() {
+    FirebaseApp.initializeApp(firebaseOptions);
+    try {
+      String[] urls = new String[]{
+          null, "", "https://firebase-db-test.firebaseio.com/foo"
+      };
+      for (String url : urls) {
+        try {
+          FirebaseDatabase.getInstance(url);
+          fail("No error thrown for URL: " + url);
+        } catch (DatabaseException expected) {
+          // expected
+        }
       }
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
     }
   }
 
   @Test
-  public void testGetInstanceForApp() throws ExecutionException, InterruptedException {
+  public void testGetInstanceForApp() {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testGetInstanceForApp");
-    FirebaseDatabase db = FirebaseDatabase.getInstance(app);
-    assertNotNull(db);
-    assertSame(db, FirebaseDatabase.getInstance(app));
+    try {
+      FirebaseDatabase db = FirebaseDatabase.getInstance(app);
+      assertNotNull(db);
+      assertSame(db, FirebaseDatabase.getInstance(app));
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
   }
 
   @Test
   public void testReference() {
-    FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
-    DatabaseReference reference = defaultDatabase.getReference();
-    assertNotNull(reference);
-    assertNull(reference.getKey());
-    assertNull(reference.getParent());
-
-    reference = defaultDatabase.getReference("foo");
-    assertNotNull(reference);
-    assertEquals("foo", reference.getKey());
-    assertNull(reference.getParent().getKey());
-
-    reference = defaultDatabase.getReference("foo/bar");
-    assertNotNull(reference);
-    assertEquals("bar", reference.getKey());
-    assertEquals("foo", reference.getParent().getKey());
+    FirebaseApp.initializeApp(firebaseOptions);
+    try {
+      FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
+      DatabaseReference reference = defaultDatabase.getReference();
+      assertNotNull(reference);
+      assertNull(reference.getKey());
+      assertNull(reference.getParent());
+
+      reference = defaultDatabase.getReference("foo");
+      assertNotNull(reference);
+      assertEquals("foo", reference.getKey());
+      assertNull(reference.getParent().getKey());
+
+      reference = defaultDatabase.getReference("foo/bar");
+      assertNotNull(reference);
+      assertEquals("bar", reference.getKey());
+      assertEquals("foo", reference.getParent().getKey());
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
   }
 
   @Test
   public void testReferenceFromUrl() {
-    FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
-    DatabaseReference reference = defaultDatabase.getReferenceFromUrl(
-        "https://firebase-db-test.firebaseio.com/foo/bar");
-    assertNotNull(reference);
-    assertEquals("bar", reference.getKey());
-    assertEquals("foo", reference.getParent().getKey());
-
+    FirebaseApp.initializeApp(firebaseOptions);
     try {
-      defaultDatabase.getReferenceFromUrl(null);
-      fail("No error thrown for null URL");
-    } catch (NullPointerException expected) {
-      // expected
-    }
+      FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
+      DatabaseReference reference = defaultDatabase.getReferenceFromUrl(
+          "https://firebase-db-test.firebaseio.com/foo/bar");
+      assertNotNull(reference);
+      assertEquals("bar", reference.getKey());
+      assertEquals("foo", reference.getParent().getKey());
+      try {
+        defaultDatabase.getReferenceFromUrl(null);
+        fail("No error thrown for null URL");
+      } catch (NullPointerException expected) {
+        // expected
+      }
 
-    try {
-      defaultDatabase.getReferenceFromUrl("https://other-db-test.firebaseio.com/foo/bar");
-      fail("No error thrown for invalid URL");
-    } catch (DatabaseException expected) {
-      // expected
+      try {
+        defaultDatabase.getReferenceFromUrl("https://other-db-test.firebaseio.com/foo/bar");
+        fail("No error thrown for invalid URL");
+      } catch (DatabaseException expected) {
+        // expected
+      }
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
     }
   }
 
   @Test
-  public void testAppDelete() throws ExecutionException, InterruptedException {
+  public void testAppDelete() {
     FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testAppDelete");
     FirebaseDatabase db = FirebaseDatabase.getInstance(app);
     assertNotNull(db);
@@ -159,16 +177,119 @@ public class FirebaseDatabaseTest {
   }
 
   @Test
-  public void testInitAfterAppDelete() throws ExecutionException, InterruptedException,
-      TimeoutException {
-    FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete");
-    FirebaseDatabase db1 = FirebaseDatabase.getInstance(app);
-    assertNotNull(db1);
-    app.delete();
+  public void testInitAfterAppDelete() {
+    try {
+      FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete");
+      FirebaseDatabase db1 = FirebaseDatabase.getInstance(app);
+      assertNotNull(db1);
+      app.delete();
+
+      app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete");
+      FirebaseDatabase db2 = FirebaseDatabase.getInstance(app);
+      assertNotNull(db2);
+      assertNotSame(db1, db2);
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
+  }
+
+  @Test
+  public void testDbUrlIsEmulatorUrlWhenSettingOptionsManually() {
+
+    List<CustomTestCase> testCases = ImmutableList.of(
+        // cases where the env var is ignored because the supplied DB URL is a valid emulator URL
+        new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "",
+            "http://my-custom-hosted-emulator.com:80", "dummy-ns"),
+        new CustomTestCase("http://localhost:9000?ns=test-ns", null,
+            "http://localhost:9000", "test-ns"),
+
+        // cases where the supplied DB URL is not an emulator URL, so we extract ns from it
+        // and append it to the emulator URL from env var(if it is valid)
+        new CustomTestCase("https://valid-namespace.firebaseio.com", "localhost:8080",
+            "http://localhost:8080", "valid-namespace"),
+        new CustomTestCase("https://test.firebaseio.com?ns=valid-namespace", "localhost:90",
+            "http://localhost:90", "valid-namespace")
+    );
+    for (CustomTestCase tc : testCases) {
+      try {
+        FirebaseApp app = FirebaseApp.initializeApp();
+        TestUtils.setEnvironmentVariables(
+            ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR,
+                Strings.nullToEmpty(tc.envVariableUrl)));
+        FirebaseDatabase instance = FirebaseDatabase.getInstance(app, tc.rootDbUrl);
+        assertEquals(tc.expectedEmulatorRootUrl,
+            instance.getReference().repo.getRepoInfo().toString());
+        assertEquals(tc.namespace, instance.getReference().repo.getRepoInfo().namespace);
+        // clean up after
+        app.delete();
+      } finally {
+        TestUtils.unsetEnvironmentVariables(
+            ImmutableSet.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR));
+      }
+    }
+  }
+
+  @Test
+  public void testDbUrlIsEmulatorUrlForDbRefWithPath() {
+
+    List<CustomTestCase> testCases = ImmutableList.of(
+        new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns",
+            "http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "",
+            "http://my-custom-hosted-emulator.com:80", "dummy-ns", "/"),
+        new CustomTestCase("http://localhost:9000?ns=test-ns",
+            "http://localhost:9000/a/b/c/d?ns=test-ns", null,
+            "http://localhost:9000", "test-ns", "/a/b/c/d"),
+        new CustomTestCase("http://localhost:9000?ns=test-ns",
+            "https://valid-namespace.firebaseio.com/a/b/c/d?ns=test-ns", "localhost:9000",
+            "http://localhost:9000", "test-ns", "/a/b/c/d"),
+        new CustomTestCase("https://valid-namespace.firebaseio.com",
+            "http://valid-namespace.firebaseio.com/a/b/c/d", "localhost:8080",
+            "http://localhost:8080", "valid-namespace", "/a/b/c/d")
+    );
+
+    for (CustomTestCase tc : testCases) {
+      try {
+        FirebaseApp app = FirebaseApp.initializeApp();
+        TestUtils.setEnvironmentVariables(
+            ImmutableMap.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR,
+                Strings.nullToEmpty(tc.envVariableUrl)));
+        FirebaseDatabase instance = FirebaseDatabase.getInstance(app, tc.rootDbUrl);
+        DatabaseReference dbRef = instance.getReferenceFromUrl(tc.pathUrl);
+        assertEquals(tc.expectedEmulatorRootUrl, dbRef.repo.getRepoInfo().toString());
+        assertEquals(tc.namespace, dbRef.repo.getRepoInfo().namespace);
+        assertEquals(tc.path, dbRef.path.toString());
+        // clean up after
+        app.delete();
+
+      } finally {
+        TestUtils.unsetEnvironmentVariables(
+            ImmutableSet.of(EmulatorHelper.FIREBASE_RTDB_EMULATOR_HOST_ENV_VAR));
+      }
+    }
+  }
+
+  private static class CustomTestCase {
 
-    app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete");
-    FirebaseDatabase db2 = FirebaseDatabase.getInstance(app);
-    assertNotNull(db2);
-    assertNotSame(db1, db2);
+    private String rootDbUrl;
+    private String pathUrl;
+    private String envVariableUrl;
+    private String expectedEmulatorRootUrl;
+    private String namespace;
+    private String path;
+
+    private CustomTestCase(String rootDbUrl, String envVariableUrl,
+        String expectedEmulatorRootUrl, String namespace) {
+      this(rootDbUrl, null, envVariableUrl, expectedEmulatorRootUrl, namespace, null);
+    }
+
+    private CustomTestCase(String rootDbUrl, String pathUrl, String envVariableUrl,
+        String expectedEmulatorRootUrl, String namespace, String path) {
+      this.rootDbUrl = rootDbUrl;
+      this.pathUrl = pathUrl;
+      this.envVariableUrl = envVariableUrl;
+      this.expectedEmulatorRootUrl = expectedEmulatorRootUrl;
+      this.namespace = namespace;
+      this.path = path;
+    }
   }
 }
diff --git a/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java b/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java
new file mode 100644
index 0000000..bffd8a5
--- /dev/null
+++ b/src/test/java/com/google/firebase/database/util/EmulatorHelperTest.java
@@ -0,0 +1,134 @@
+package com.google.firebase.database.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.firebase.database.DatabaseException;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class EmulatorHelperTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testExtractingEmulatorUrlFromSuppliedUrlSucceeds() {
+    Map<String, String> suppliedToExpectedUrlsMap = ImmutableMap.of(
+        "http://localhost:9000?ns=test-ns", "http://localhost:9000/?ns=test-ns",
+        "http://my-custom-hosted-emulator.com:80?ns=dummy-ns",
+        "http://my-custom-hosted-emulator.com:80/?ns=dummy-ns",
+        "http://my-custom-hosted-emulator.com:80/path/to/document?ns=dummy-ns",
+        "http://my-custom-hosted-emulator.com:80/path/to/document/?ns=dummy-ns",
+        "https://localhost:9000?ns=test-ns", "https://localhost:9000/?ns=test-ns",
+        "http://my-custom-hosted-emulator.com?ns=dummy-ns",
+        "http://my-custom-hosted-emulator.com/?ns=dummy-ns"
+    );
+    for (Map.Entry<String, String> e : suppliedToExpectedUrlsMap.entrySet()) {
+      assertTrue(EmulatorHelper.isEmulatorUrl(e.getKey()));
+    }
+  }
+
+  @Test
+  public void testExtractingEmulatorUrlsFromSuppliedUrlFails() {
+    List<String> nonEmulatorUrls = ImmutableList.of(
+        "http://localhost:9000",// missing ns param
+        "http://my-custom-hosted-emulator.com:80", // missing ns param
+        "http://test-namespace.firebaseio.com" // firebaseio.com not supported
+    );
+    for (String url : nonEmulatorUrls) {
+      assertFalse(EmulatorHelper.isEmulatorUrl(url));
+    }
+  }
+
+  @Test
+  public void testExtractingEmulatorUrlsThrowsException() {
+    List<String> invalidFormedUrls = ImmutableList.of(
+        "localhost",
+        "localhost:9000"
+    );
+    for (String invalidFormedUrl : invalidFormedUrls) {
+      thrown.expect(DatabaseException.class);
+      EmulatorHelper.isEmulatorUrl(invalidFormedUrl);
+    }
+  }
+
+  @Test
+  public void testEmulatorUrlCorrectlyPickedUp() {
+    class CustomTestCase {
+
+      private String suppliedDbUrl;
+      private String envVariableUrl;
+      private String expectedEmulatorUrl;
+
+      private CustomTestCase(String suppliedDbUrl, String envVariableUrl,
+          String expectedEmulatorUrl) {
+        this.suppliedDbUrl = suppliedDbUrl;
+        this.envVariableUrl = envVariableUrl;
+        this.expectedEmulatorUrl = expectedEmulatorUrl;
+      }
+    }
+
+    List<CustomTestCase> testCases; // separated declaration and assignment coz of checkstyle
+    testCases = ImmutableList.of(
+        // cases where the env var is ignored because the supplied DB URL is a valid emulator URL
+        new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns", "",
+            "http://my-custom-hosted-emulator.com:80?ns=dummy-ns"),
+        new CustomTestCase("http://localhost:9000?ns=test-ns", null,
+            "http://localhost:9000?ns=test-ns"),
+        new CustomTestCase("http://my-custom-hosted-emulator.com:80?ns=dummy-ns",
+            "http://localhost:8080?ns=ns-2",
+            "http://my-custom-hosted-emulator.com:80?ns=dummy-ns"),
+        new CustomTestCase("http://localhost:9000?ns=ns-1", "localhost:8080",
+            "http://localhost:9000?ns=ns-1"),
+        new CustomTestCase("http://localhost:9000?ns=ns-1", "http://localhost:8080/ns=ns-2",
+            "http://localhost:9000?ns=ns-1"),
+        new CustomTestCase("http://localhost:9000/a/b/c?ns=ns-1", "http://localhost:8080/ns=ns-2",
+            "http://localhost:9000/a/b/c?ns=ns-1"),
+        new CustomTestCase("https://firebaseio.com?ns=valid-namespace", "localhost:90",
+            "https://firebaseio.com?ns=valid-namespace"),
+
+        // cases where the supplied DB URL is not an emulator URL, so we extract ns from it
+        // and append it to the emulator URL from env var(if it is valid)
+        new CustomTestCase("https://valid-namespace.firebaseio.com", "localhost:8080",
+            "http://localhost:8080/?ns=valid-namespace"),
+        new CustomTestCase("https://valid-namespace.firebaseio.com", "custom-emulator-url:90",
+            "http://custom-emulator-url:90/?ns=valid-namespace"),
+        new CustomTestCase("https://valid-namespace.firebaseio.com/a/b/c", "custom-emulator-url:90",
+            "http://custom-emulator-url:90/a/b/c/?ns=valid-namespace"),
+        new CustomTestCase("https://valid-namespace.firebaseio.com", "192.123.212.145:90",
+            "http://192.123.212.145:90/?ns=valid-namespace"),
+        new CustomTestCase("https://valid-namespace.firebaseio.com", "[::1]:90",
+            "http://[::1]:90/?ns=valid-namespace"),
+        new CustomTestCase(null, "localhost:90", "http://localhost:90/?ns=default"),
+        new CustomTestCase("", "localhost:90", "http://localhost:90/?ns=default")
+    );
+
+    for (CustomTestCase tc : testCases) {
+      assertEquals(tc.expectedEmulatorUrl,
+          EmulatorHelper.getEmulatorUrl(tc.suppliedDbUrl, tc.envVariableUrl));
+    }
+  }
+
+  @Test
+  public void testInvalidEmulatorUrlFromEnvVarThrows() {
+    List<String> invalidEnvVars = ImmutableList.of(
+        "http://localhost:8080",
+        "http://localhost:8080?ns=test-ns",
+        "localhost"
+    );
+    for (String invalidEnvVar : invalidEnvVars) {
+      thrown.expect(IllegalArgumentException.class);
+      EmulatorHelper.getEmulatorUrl("https://valid-namespace.firebaseio.com",
+          invalidEnvVar);
+    }
+  }
+}
+
+
diff --git a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java
index f4efbad..0dcc1cb 100644
--- a/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java
+++ b/src/test/java/com/google/firebase/database/utilities/UtilitiesTest.java
@@ -32,6 +32,9 @@ import com.google.firebase.database.DatabaseError;
 import com.google.firebase.database.DatabaseException;
 import com.google.firebase.database.DatabaseReference;
 import com.google.firebase.database.DatabaseReference.CompletionListener;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
@@ -74,6 +77,15 @@ public class UtilitiesTest {
     assertTrue(url.repoInfo.isSecure());
     assertEquals("test", url.repoInfo.namespace);
     assertEquals(ImmutableList.of("foo", "bar"), url.path.asList());
+
+    url = Utilities
+        .parseUrl("https://firebaseio.com/path%20with%20spaces/?ns=random%20valid%20namespace");
+    assertTrue(url.repoInfo.isSecure());
+    assertEquals("random valid namespace", url.repoInfo.namespace);
+    assertEquals("/path%20with%20spaces", url.path.toString());
+
+    url = Utilities.parseUrl("http://test.firebaseio.com/+");
+    assertEquals("/ ", url.path.toString());
   }
 
   @Test
@@ -185,4 +197,21 @@ public class UtilitiesTest {
     assertSame(listener, result.getSecond());
   }
 
+  @Test
+  public void testExtractParamsFromUrl() throws UnsupportedEncodingException {
+    Map<String, String> params = Utilities.getQueryParamsMap("abc=213&qpf=2312&xyz=true&qpf=hi");
+    assertEquals("213", params.get("abc"));
+    assertEquals("2312,hi", params.get("qpf"));
+    assertEquals("true", params.get("xyz"));
+
+    params = Utilities.getQueryParamsMap(
+        "q=a%3D2%26b%3D3&oq=a%3D2%26b%3D3&aqs=chrome..69i57j0l5.4023j0j7&sourceid=chrome&ie=UTF-8");
+    assertEquals("a=2&b=3", params.get("q"));
+    assertEquals("a=2&b=3", params.get("oq"));
+    assertEquals("chrome", params.get("sourceid"));
+
+    params = Utilities.getQueryParamsMap("a=%3F%3F%3F&b=%3D%26%3D");
+    assertEquals("???", params.get("a"));
+    assertEquals("=&=", params.get("b"));
+  }
 }
diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java
index f8e1b4d..a63aa8d 100644
--- a/src/test/java/com/google/firebase/testing/TestUtils.java
+++ b/src/test/java/com/google/firebase/testing/TestUtils.java
@@ -43,6 +43,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** Test Utils for use by all tests (both unit and integration tests). */
 public class TestUtils {
@@ -85,6 +86,30 @@ public class TestUtils {
     }
   }
 
+  public static void unsetEnvironmentVariables(Set<String> vars) {
+    // Unsetting the environment variables after the JVM has started requires a bit of a hack:
+    // we reach into the package-private java.lang.ProcessEnvironment class, which incidentally
+    // is platform-specific, and replace the map held in a static final field there,
+    // using yet more reflection.
+    //
+    // This is copied from {#see com.google.apphosting.runtime.NullSandboxPlugin}
+    Map<String, String> allVars = new HashMap<>(System.getenv());
+    for (String var : vars) {
+      allVars.remove(var);
+    }
+    try {
+      Class<?> pe = Class.forName("java.lang.ProcessEnvironment", true, null);
+      Field f = pe.getDeclaredField("theUnmodifiableEnvironment");
+      f.setAccessible(true);
+      Field m = Field.class.getDeclaredField("modifiers");
+      m.setAccessible(true);
+      m.setInt(f, m.getInt(f) & ~Modifier.FINAL);
+      f.set(null, Collections.unmodifiableMap(allVars));
+    } catch (ReflectiveOperationException e) {
+      throw new RuntimeException("failed to unset the environment variables", e);
+    }
+  }
+
   public static String loadResource(String path) {
     InputStream stream = TestUtils.class.getClassLoader().getResourceAsStream(path);
     checkNotNull(stream, "Failed to load resource: %s", path);