(TWILL-158) Added FileContext Location and LocationFactory

- Added new unit-tests.
- Minor improvement on HDFSLocationFactory to handle creation URI correctly

This closes #73 on GitHub.

Signed-off-by: Terence Yim <chtyim@apache.org>
diff --git a/twill-common/src/main/java/org/apache/twill/filesystem/LocalLocationFactory.java b/twill-common/src/main/java/org/apache/twill/filesystem/LocalLocationFactory.java
index 82847b2..8e7ab8b 100644
--- a/twill-common/src/main/java/org/apache/twill/filesystem/LocalLocationFactory.java
+++ b/twill-common/src/main/java/org/apache/twill/filesystem/LocalLocationFactory.java
@@ -31,7 +31,7 @@
    * Constructs a LocalLocationFactory that Location created will be relative to system root.
    */
   public LocalLocationFactory() {
-    this(new File("/"));
+    this(new File(File.separator));
   }
 
   public LocalLocationFactory(File basePath) {
@@ -40,7 +40,7 @@
 
   @Override
   public Location create(String path) {
-    return new LocalLocation(this, new File(basePath, path));
+    return create(new File(basePath, path).toURI());
   }
 
   @Override
diff --git a/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocation.java b/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocation.java
new file mode 100644
index 0000000..f92954e
--- /dev/null
+++ b/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocation.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.twill.filesystem;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import org.apache.hadoop.fs.CreateFlag;
+import org.apache.hadoop.fs.FileAlreadyExistsException;
+import org.apache.hadoop.fs.FileContext;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.Options;
+import org.apache.hadoop.fs.ParentNotDirectoryException;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.RemoteIterator;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.hdfs.HAUtil;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import javax.annotation.Nullable;
+
+/**
+ * An implementation of {@link Location} using {@link FileContext}.
+ */
+final class FileContextLocation implements Location {
+
+  private final FileContextLocationFactory locationFactory;
+  private final FileContext fc;
+  private final Path path;
+
+  FileContextLocation(FileContextLocationFactory locationFactory, FileContext fc, Path path) {
+    this.locationFactory = locationFactory;
+    this.fc = fc;
+    this.path = path;
+  }
+
+  @Override
+  public boolean exists() throws IOException {
+    return fc.util().exists(path);
+  }
+
+  @Override
+  public String getName() {
+    return path.getName();
+  }
+
+  @Override
+  public boolean createNew() throws IOException {
+    try {
+      fc.create(path, EnumSet.of(CreateFlag.CREATE), Options.CreateOpts.createParent()).close();
+      return true;
+    } catch (FileAlreadyExistsException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public InputStream getInputStream() throws IOException {
+    return fc.open(path);
+  }
+
+  @Override
+  public OutputStream getOutputStream() throws IOException {
+    return fc.create(path, EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE), Options.CreateOpts.createParent());
+  }
+
+  @Override
+  public OutputStream getOutputStream(String permission) throws IOException {
+    return fc.create(path, EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE),
+                     Options.CreateOpts.perms(new FsPermission(permission)),
+                     Options.CreateOpts.createParent());
+  }
+
+  @Override
+  public Location append(String child) throws IOException {
+    if (child.startsWith("/")) {
+      child = child.substring(1);
+    }
+    return new FileContextLocation(locationFactory, fc, new Path(URI.create(path.toUri() + "/" + child)));
+  }
+
+  @Override
+  public Location getTempFile(String suffix) throws IOException {
+    Path path = new Path(
+      URI.create(this.path.toUri() + "." + UUID.randomUUID() + (suffix == null ? TEMP_FILE_SUFFIX : suffix)));
+    return new FileContextLocation(locationFactory, fc, path);
+  }
+
+  @Override
+  public URI toURI() {
+    // In HA mode, the path URI returned by path created through FileContext is incompatible with the FileSystem,
+    // which is used inside Hadoop. It is due to the fact that FileContext is not HA aware and it always
+    // append "port" to the path URI, while the DistributedFileSystem always use the cluster logical
+    // name, which doesn't allow having port in it.
+    URI uri = path.toUri();
+    if (HAUtil.isLogicalUri(locationFactory.getConfiguration(), uri)) {
+      try {
+        // Need to strip out the port if in HA
+        return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(),
+                       -1, uri.getPath(), uri.getQuery(), uri.getFragment());
+      } catch (URISyntaxException e) {
+        // Shouldn't happen
+        throw Throwables.propagate(e);
+      }
+    }
+
+    return uri;
+  }
+
+  @Override
+  public boolean delete() throws IOException {
+    return delete(false);
+  }
+
+  @Override
+  public boolean delete(boolean recursive) throws IOException {
+    return fc.delete(path, recursive);
+  }
+
+  @Nullable
+  @Override
+  public Location renameTo(Location destination) throws IOException {
+    Path targetPath = new Path(destination.toURI());
+    try {
+      fc.rename(path, targetPath, Options.Rename.OVERWRITE);
+      return new FileContextLocation(locationFactory, fc, targetPath);
+    } catch (FileAlreadyExistsException | FileNotFoundException | ParentNotDirectoryException e) {
+      return null;
+    }
+  }
+
+  @Override
+  public boolean mkdirs() throws IOException {
+    try {
+      fc.mkdir(path, null, true);
+      return true;
+    } catch (FileAlreadyExistsException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public long length() throws IOException {
+    return fc.getFileStatus(path).getLen();
+  }
+
+  @Override
+  public long lastModified() throws IOException {
+    return fc.getFileStatus(path).getModificationTime();
+  }
+
+  @Override
+  public boolean isDirectory() throws IOException {
+    try {
+      return fc.getFileStatus(path).isDirectory();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public List<Location> list() throws IOException {
+    RemoteIterator<FileStatus> statuses = fc.listStatus(path);
+    ImmutableList.Builder<Location> result = ImmutableList.builder();
+    while (statuses.hasNext()) {
+      FileStatus status = statuses.next();
+      if (!Objects.equals(path, status.getPath())) {
+        result.add(new FileContextLocation(locationFactory, fc, status.getPath()));
+      }
+    }
+    return result.build();
+
+  }
+
+  @Override
+  public LocationFactory getLocationFactory() {
+    return locationFactory;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    FileContextLocation that = (FileContextLocation) o;
+    return Objects.equals(path, that.path);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(path);
+  }
+}
diff --git a/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocationFactory.java b/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocationFactory.java
new file mode 100644
index 0000000..d64be71
--- /dev/null
+++ b/twill-yarn/src/main/java/org/apache/twill/filesystem/FileContextLocationFactory.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.twill.filesystem;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileContext;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.UnsupportedFileSystemException;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * A {@link LocationFactory} implementation that uses {@link FileContext} to create {@link Location}.
+ */
+public class FileContextLocationFactory implements LocationFactory {
+
+  private final Configuration configuration;
+  private final FileContext fc;
+  private final Path pathBase;
+
+  /**
+   * Same as {@link #FileContextLocationFactory(Configuration, String) FileContextLocationFactory(configuration, "/")}.
+   */
+  public FileContextLocationFactory(Configuration configuration) {
+    this(configuration, "/");
+  }
+
+  /**
+   * Creates a new instance.
+   *
+   * @param configuration the hadoop configuration
+   * @param pathBase base path for all non-absolute location created through this {@link LocationFactory}.
+   */
+  public FileContextLocationFactory(Configuration configuration, String pathBase) {
+    this.configuration = configuration;
+    this.fc = createFileContext(configuration);
+    this.pathBase = new Path(pathBase.startsWith("/") ? pathBase : "/" + pathBase);
+  }
+
+  @Override
+  public Location create(String path) {
+    if (path.startsWith("/")) {
+      path = path.substring(1);
+    }
+    Path locationPath;
+    if (path.isEmpty()) {
+      locationPath = pathBase;
+    } else {
+      locationPath = new Path(path);
+    }
+    locationPath = locationPath.makeQualified(fc.getDefaultFileSystem().getUri(), pathBase);
+    return new FileContextLocation(this, fc, locationPath);
+  }
+
+  @Override
+  public Location create(URI uri) {
+    URI contextURI = fc.getWorkingDirectory().toUri();
+    if (Objects.equals(contextURI.getScheme(), uri.getScheme())
+      && Objects.equals(contextURI.getAuthority(), uri.getAuthority())) {
+      // A full URI
+      return new FileContextLocation(this, fc, new Path(uri));
+    }
+
+    if (uri.isAbsolute()) {
+      // Needs to be of the same scheme
+      Preconditions.checkArgument(Objects.equals(contextURI.getScheme(), uri.getScheme()),
+                                  "Only URI with '%s' scheme is supported", contextURI.getScheme());
+      Path locationPath = new Path(uri).makeQualified(fc.getDefaultFileSystem().getUri(), pathBase);
+      return new FileContextLocation(this, fc, locationPath);
+    }
+
+    return create(uri.getPath());
+  }
+
+  @Override
+  public Location getHomeLocation() {
+    return new FileContextLocation(this, fc, fc.getHomeDirectory());
+  }
+
+  /**
+   * Returns the {@link FileContext} used by this {@link LocationFactory}.
+   */
+  public FileContext getFileContext() {
+    return fc;
+  }
+
+  /**
+   * Returns the {@link Configuration} used by this {@link LocationFactory}.
+   */
+  public Configuration getConfiguration() {
+    return configuration;
+  }
+
+  private static FileContext createFileContext(Configuration configuration) {
+    try {
+      return FileContext.getFileContext(configuration);
+    } catch (UnsupportedFileSystemException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
diff --git a/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocation.java b/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocation.java
index 818fe23..aa29384 100644
--- a/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocation.java
+++ b/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocation.java
@@ -35,7 +35,7 @@
 import java.util.UUID;
 
 /**
- * A concrete implementation of {@link Location} for the HDFS filesystem.
+ * A concrete implementation of {@link Location} for the HDFS filesystem using {@link FileSystem}.
  */
 final class HDFSLocation implements Location {
   private final FileSystem fs;
diff --git a/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocationFactory.java b/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocationFactory.java
index 65146a8..728de32 100644
--- a/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocationFactory.java
+++ b/twill-yarn/src/main/java/org/apache/twill/filesystem/HDFSLocationFactory.java
@@ -17,6 +17,7 @@
  */
 package org.apache.twill.filesystem;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
@@ -24,10 +25,14 @@
 
 import java.io.IOException;
 import java.net.URI;
+import java.util.Objects;
 
 /**
- * A {@link LocationFactory} that creates HDFS {@link Location}.
+ * A {@link LocationFactory} that creates HDFS {@link Location} using {@link FileSystem}.
+ *
+ * @deprecated Deprecated since 0.7.0. Use {@link FileContextLocationFactory} instead.
  */
+@Deprecated
 public final class HDFSLocationFactory implements LocationFactory {
 
   private final FileSystem fileSystem;
@@ -63,14 +68,21 @@
 
   @Override
   public Location create(URI uri) {
-    if (!uri.toString().startsWith(fileSystem.getUri().toString())) {
+    URI fsURI = fileSystem.getUri();
+    if (Objects.equals(fsURI.getScheme(), uri.getScheme())
+      && Objects.equals(fsURI.getAuthority(), uri.getAuthority())) {
       // It's a full URI
       return new HDFSLocation(this, new Path(uri));
     }
+
     if (uri.isAbsolute()) {
+      // Needs to be of the same scheme
+      Preconditions.checkArgument(Objects.equals(fsURI.getScheme(), uri.getScheme()),
+                                  "Only URI with '%s' scheme is supported", fsURI.getScheme());
       return new HDFSLocation(this, new Path(fileSystem.getUri() + uri.getPath()));
     }
-    return new HDFSLocation(this, new Path(fileSystem.getUri() + "/" + pathBase + "/" + uri.getPath()));
+
+    return create(uri.getPath());
   }
 
   @Override
diff --git a/twill-yarn/src/test/java/org/apache/twill/filesystem/FileContextLocationTest.java b/twill-yarn/src/test/java/org/apache/twill/filesystem/FileContextLocationTest.java
new file mode 100644
index 0000000..e4c3774
--- /dev/null
+++ b/twill-yarn/src/test/java/org/apache/twill/filesystem/FileContextLocationTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.twill.filesystem;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hdfs.MiniDFSCluster;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+
+/**
+ *
+ */
+public class FileContextLocationTest extends LocationTestBase {
+
+  private static MiniDFSCluster dfsCluster;
+
+  @BeforeClass
+  public static void init() throws IOException {
+    Configuration conf = new Configuration();
+    conf.set(MiniDFSCluster.HDFS_MINIDFS_BASEDIR, tmpFolder.newFolder().getAbsolutePath());
+    dfsCluster = new MiniDFSCluster.Builder(conf).numDataNodes(1).build();
+  }
+
+  @AfterClass
+  public static void finish() {
+    dfsCluster.shutdown();
+  }
+
+  @Override
+  protected LocationFactory createLocationFactory(String pathBase) throws Exception {
+    return new FileContextLocationFactory(dfsCluster.getFileSystem().getConf(), pathBase);
+  }
+}
diff --git a/twill-yarn/src/test/java/org/apache/twill/filesystem/HDFSLocationTest.java b/twill-yarn/src/test/java/org/apache/twill/filesystem/HDFSLocationTest.java
index 20f7403..d57d49f 100644
--- a/twill-yarn/src/test/java/org/apache/twill/filesystem/HDFSLocationTest.java
+++ b/twill-yarn/src/test/java/org/apache/twill/filesystem/HDFSLocationTest.java
@@ -30,14 +30,12 @@
 public class HDFSLocationTest extends LocationTestBase {
 
   private static MiniDFSCluster dfsCluster;
-  private static LocationFactory locationFactory;
 
   @BeforeClass
   public static void init() throws IOException {
     Configuration conf = new Configuration();
     conf.set(MiniDFSCluster.HDFS_MINIDFS_BASEDIR, tmpFolder.newFolder().getAbsolutePath());
     dfsCluster = new MiniDFSCluster.Builder(conf).numDataNodes(1).build();
-    locationFactory = new HDFSLocationFactory(dfsCluster.getFileSystem());
   }
 
   @AfterClass
@@ -46,7 +44,7 @@
   }
 
   @Override
-  protected LocationFactory getLocationFactory() {
-    return locationFactory;
+  protected LocationFactory createLocationFactory(String pathBase) throws Exception {
+    return new HDFSLocationFactory(dfsCluster.getFileSystem(), pathBase);
   }
 }
diff --git a/twill-yarn/src/test/java/org/apache/twill/filesystem/LocalLocationTest.java b/twill-yarn/src/test/java/org/apache/twill/filesystem/LocalLocationTest.java
index 3f6d931..ba21beb 100644
--- a/twill-yarn/src/test/java/org/apache/twill/filesystem/LocalLocationTest.java
+++ b/twill-yarn/src/test/java/org/apache/twill/filesystem/LocalLocationTest.java
@@ -17,24 +17,17 @@
  */
 package org.apache.twill.filesystem;
 
-import org.junit.BeforeClass;
-
-import java.io.IOException;
+import java.io.File;
 
 /**
  *
  */
 public class LocalLocationTest extends LocationTestBase {
 
-  private static LocationFactory locationFactory;
-
-  @BeforeClass
-  public static void init() throws IOException {
-    locationFactory = new LocalLocationFactory(tmpFolder.newFolder());
-  }
-
   @Override
-  protected LocationFactory getLocationFactory() {
-    return locationFactory;
+  protected LocationFactory createLocationFactory(String pathBase) throws Exception {
+    File basePath = new File(tmpFolder.newFolder(), pathBase);
+    basePath.mkdirs();
+    return new LocalLocationFactory(basePath);
   }
 }
diff --git a/twill-yarn/src/test/java/org/apache/twill/filesystem/LocationTestBase.java b/twill-yarn/src/test/java/org/apache/twill/filesystem/LocationTestBase.java
index ee591e7..e01115b 100644
--- a/twill-yarn/src/test/java/org/apache/twill/filesystem/LocationTestBase.java
+++ b/twill-yarn/src/test/java/org/apache/twill/filesystem/LocationTestBase.java
@@ -17,13 +17,23 @@
  */
 package org.apache.twill.filesystem;
 
+import com.google.common.base.Charsets;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.CharStreams;
 import org.junit.Assert;
 import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.List;
 
 /**
@@ -34,10 +44,60 @@
   @ClassRule
   public static TemporaryFolder tmpFolder = new TemporaryFolder();
 
+  private final LoadingCache<String, LocationFactory> locationFactoryCache = CacheBuilder.newBuilder()
+    .build(new CacheLoader<String, LocationFactory>() {
+      @Override
+      public LocationFactory load(String key) throws Exception {
+        return createLocationFactory(key);
+      }
+    });
+
+  @Test
+  public void testBasic() throws Exception {
+    LocationFactory factory = locationFactoryCache.getUnchecked("basic");
+    URI baseURI = factory.create("/").toURI();
+
+    // Test basic location construction
+    Assert.assertEquals(factory.create("/file"), factory.create("/file"));
+    Assert.assertEquals(factory.create("/file2"),
+                        factory.create(URI.create(baseURI.getScheme() + ":" + baseURI.getPath() + "/file2")));
+    Assert.assertEquals(factory.create("/file3"),
+                        factory.create(
+                          new URI(baseURI.getScheme(), baseURI.getAuthority(),
+                                  baseURI.getPath() + "/file3", null, null)));
+    Assert.assertEquals(factory.create("/"), factory.create("/"));
+    Assert.assertEquals(factory.create("/"), factory.create(URI.create(baseURI.getScheme() + ":" + baseURI.getPath())));
+
+    Assert.assertEquals(factory.create("/"),
+                        factory.create(new URI(baseURI.getScheme(), baseURI.getAuthority(),
+                                               baseURI.getPath(), null, null)));
+
+    // Test file creation and rename
+    Location location = factory.create("/file");
+    Assert.assertTrue(location.createNew());
+    Assert.assertTrue(location.exists());
+
+    Location location2 = factory.create("/file2");
+    String message = "Testing Message";
+    try (Writer writer = new OutputStreamWriter(location2.getOutputStream(), Charsets.UTF_8)) {
+      writer.write(message);
+    }
+    long length = location2.length();
+    long lastModified = location2.lastModified();
+
+    location2.renameTo(location);
+
+    Assert.assertFalse(location2.exists());
+    try (Reader reader = new InputStreamReader(location.getInputStream(), Charsets.UTF_8)) {
+      Assert.assertEquals(message, CharStreams.toString(reader));
+    }
+    Assert.assertEquals(length, location.length());
+    Assert.assertEquals(lastModified, location.lastModified());
+  }
 
   @Test
   public void testDelete() throws IOException {
-    LocationFactory factory = getLocationFactory();
+    LocationFactory factory = locationFactoryCache.getUnchecked("delete");
 
     Location base = factory.create("test").getTempFile(".tmp");
     Assert.assertTrue(base.mkdirs());
@@ -57,7 +117,7 @@
 
   @Test
   public void testHelper() {
-    LocationFactory factory = LocationFactories.namespace(getLocationFactory(), "testhelper");
+    LocationFactory factory = LocationFactories.namespace(locationFactoryCache.getUnchecked("helper"), "testhelper");
 
     Location location = factory.create("test");
     Assert.assertTrue(location.toURI().getPath().endsWith("testhelper/test"));
@@ -68,7 +128,7 @@
 
   @Test
   public void testList() throws IOException {
-    LocationFactory factory = getLocationFactory();
+    LocationFactory factory = locationFactoryCache.getUnchecked("list");
 
     Location dir = factory.create("dir");
 
@@ -107,5 +167,5 @@
     }
   }
 
-  protected abstract LocationFactory getLocationFactory();
+  protected abstract LocationFactory createLocationFactory(String pathBase) throws Exception;
 }