Skip to content

Commit abd3805

Browse files
authored
Merge pull request #152 from saalfeldlab/fix/uriResolveJava8
Fix/uri resolve java8
2 parents e8d9232 + b7dbd6f commit abd3805

File tree

6 files changed

+160
-48
lines changed

6 files changed

+160
-48
lines changed

src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ public String compose(final String... components) {
325325
else
326326
composedPath = Paths.get(uri.toString());
327327
for (String component : components) {
328+
if (component == null || component.isEmpty())
329+
continue;
328330
composedPath = composedPath.resolve(component);
329331
}
330332

src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,28 @@ public default String[] components(final String path) {
8080
*/
8181
public default String compose(final URI uri, final String... components) {
8282

83-
if (components.length == 0)
83+
int firstNonEmptyIdx = 0;
84+
while (firstNonEmptyIdx < components.length && (components[firstNonEmptyIdx] == null || components[firstNonEmptyIdx].isEmpty())) {
85+
firstNonEmptyIdx++;
86+
}
87+
88+
/*If there are no non-empty components, there is nothing to compose against; return the uri. */
89+
if (components.length == firstNonEmptyIdx)
8490
return uri.toString();
8591

86-
/* add the initial path to the components */
87-
final String[] allComponents = new String[components.length + 1];
88-
allComponents[0] = uri.getPath();
89-
System.arraycopy(components, 0, allComponents, 1, components.length);
92+
/* allocate space for the initial path and the new components, skipping empty strings */
93+
final int nonEmptysize = components.length - firstNonEmptyIdx;
94+
final String[] allComponents = new String[1 + nonEmptysize];
95+
if (uri.getPath().isEmpty())
96+
//TODO Caleb: This `isEmpty()` check is only necessary for Java 8. In newer versions
97+
// URI resolution is updated so that resolving and empty path with a new path adds
98+
// a leading `/` between the rest of the URI and the path part. In Java 8 it doesn't
99+
// add the `/` so it ends up directly concatenating the path part with URI
100+
allComponents[0] = "/";
101+
else
102+
allComponents[0] = uri.getPath();
103+
104+
System.arraycopy(components, firstNonEmptyIdx, allComponents, 1, nonEmptysize);
90105

91106
URI composedUri = uri;
92107
for (int i = 0; i < allComponents.length; i++) {

src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.util.List;
4646
import java.util.Map;
4747
import java.util.Random;
48+
import java.util.UUID;
4849
import java.util.concurrent.ExecutionException;
4950
import java.util.concurrent.Executors;
5051
import java.util.function.Predicate;
@@ -1074,12 +1075,16 @@ public void testVersion() throws NumberFormatException, IOException, URISyntaxEx
10741075
@Test
10751076
public void testReaderCreation() throws IOException, URISyntaxException {
10761077

1077-
final String location;
1078-
N5Writer removeMe = null;
1079-
try (N5Writer writer = createN5Writer()) {
1080-
removeMe = writer;
1081-
location = writer.getURI().toString();
1078+
// non-existent location should fail
1079+
final String location = tempN5Location() + "-" + UUID.randomUUID();
1080+
assertThrows("Non-existent location throws error", N5Exception.N5IOException.class,
1081+
() -> {
1082+
try (N5Reader test = createN5Reader(location)) {
1083+
test.list("/");
1084+
}
1085+
});
10821086

1087+
try (N5Writer writer = createTempN5Writer(location)) {
10831088
try (N5Reader n5r = createN5Reader(location)) {
10841089
assertNotNull(n5r);
10851090
}
@@ -1107,17 +1112,7 @@ public void testReaderCreation() throws IOException, URISyntaxException {
11071112
/*Only try with resource to ensure `close()` is called.*/
11081113
}
11091114
});
1110-
} finally {
1111-
removeMe.remove();
11121115
}
1113-
1114-
// non-existent location should fail
1115-
assertThrows("Non-existent location throws error", N5Exception.N5IOException.class,
1116-
() -> {
1117-
try (N5Reader test = createN5Reader(location)) {
1118-
test.list("/");
1119-
}
1120-
});
11211116
}
11221117

11231118
@Test

src/test/java/org/janelia/saalfeldlab/n5/N5CachedFSTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.nio.file.Paths;
1616
import java.util.ArrayList;
1717
import java.util.Arrays;
18+
import java.util.HashSet;
1819
import java.util.List;
1920
import java.util.Set;
2021
import java.util.stream.Collectors;
@@ -422,8 +423,8 @@ public static void cacheBehaviorHelper(final TrackingStorage n5) throws IOExcept
422423
assertEquals(++expectedAttributeCount, n5.getAttrCallCount());
423424
assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount());
424425

425-
final Set<String> listSet = Arrays.stream(n5.list("a")).collect(Collectors.toSet());
426-
assertEquals(Stream.of("a", "b", "c").collect(Collectors.toSet()), listSet);
426+
final Set<String> abcListSet = Arrays.stream(n5.list("a")).collect(Collectors.toSet());
427+
assertEquals(Stream.of("a", "b", "c").collect(Collectors.toSet()), abcListSet);
427428
assertEquals(expectedGroupCount, n5.getGroupCallCount());
428429
assertEquals(expectedDatasetCount, n5.getDatasetCallCount());
429430
assertEquals(expectedAttributeCount, n5.getAttrCallCount());
@@ -432,7 +433,8 @@ public static void cacheBehaviorHelper(final TrackingStorage n5) throws IOExcept
432433

433434
// remove a
434435
n5.remove("a/a");
435-
assertArrayEquals(new String[] {"b", "c"}, n5.list("a")); // call list
436+
final Set<String> bc = Arrays.stream(n5.list("a")).collect(Collectors.toSet());
437+
assertEquals(Stream.of("b", "c").collect(Collectors.toSet()), bc);
436438
assertEquals(expectedExistCount, n5.getExistCallCount());
437439
assertEquals(expectedGroupCount, n5.getGroupCallCount());
438440
assertEquals(expectedDatasetCount, n5.getDatasetCallCount());

src/test/java/org/janelia/saalfeldlab/n5/kva/AbstractKeyValueAccessTest.java

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.junit.Test;
99

1010
import java.net.URI;
11+
import java.util.LinkedHashSet;
12+
import java.util.Set;
1113

1214
import static org.junit.Assert.assertArrayEquals;
1315
import static org.junit.Assert.assertEquals;
@@ -18,31 +20,43 @@
1820
public abstract class AbstractKeyValueAccessTest {
1921

2022
protected abstract KeyValueAccess newKeyValueAccess(URI root);
23+
2124
protected KeyValueAccess newKeyValueAccess() {
25+
2226
return newKeyValueAccess(tempUri());
2327
}
2428

2529
protected abstract URI tempUri();
2630

2731
protected URI[] testURIs(final URI base) {
32+
33+
final Set<URI> testUris = new LinkedHashSet<>();
34+
/*add the base uri as a test case */
35+
testUris.add(base);
36+
37+
/* NOTE: Java 8 doesn't behave well with URIs with empty path when resolving against a path.
38+
* See KeyValueAccess#compose for more details.
39+
* In tests with that as a base URI, resolve against `/` first.
40+
* Should be unnecessary in Java 21*/
41+
final URI testUri = base.getPath().isEmpty() ? base.resolve("/") : base;
2842
final URI[] pathParts = new URI[]{
29-
N5URI.getAsUri("test/path/file"), // typical path, with leading and trailing slash
30-
N5URI.getAsUri("test/path/file/"), // typical path, with leading and trailing slash
31-
N5URI.getAsUri("/test/path/file/"), // typical path, with leading and trailing slash
32-
N5URI.getAsUri("file"), // single path
33-
N5URI.getAsUri("file/"), // single path
34-
N5URI.getAsUri("/file/"), // single path
43+
N5URI.getAsUri("test/path/file"), // typical path, with no leading or trailing slashes
44+
N5URI.getAsUri("test/path/file/"), // typical path, with trailing slash
45+
N5URI.getAsUri("/test/path/file"), // typical path, with leading slash
46+
N5URI.getAsUri("/test/path/file/"), // typical path, with leading and trailing slash
47+
N5URI.getAsUri("file"), // single path
48+
N5URI.getAsUri("file/"), // single path
49+
N5URI.getAsUri("/file"), // single path
50+
N5URI.getAsUri("/file/"), // single path
3551
N5URI.getAsUri("path/w i t h/spaces"),
3652
N5URI.getAsUri("uri/illegal%character"),
37-
N5URI.getAsUri("/"),
38-
N5URI.getAsUri("")
53+
N5URI.getAsUri("/"), // root path
54+
N5URI.getAsUri("") // empty path
3955
};
40-
final URI[] testUris = new URI[pathParts.length ];
41-
for (int i = 0; i < pathParts.length; i ++) {
42-
final URI pathPart = pathParts[i];
43-
testUris[i] = base.resolve(pathPart);
56+
for (final URI pathPart : pathParts) {
57+
testUris.add(testUri.resolve(pathPart));
4458
}
45-
return testUris;
59+
return testUris.toArray(new URI[0]);
4660
}
4761

4862
protected String[][] testPathComponents(final URI base) {
@@ -81,7 +95,7 @@ protected void testComponentsAtLocation(URI testRoot) {
8195

8296
final String[] components = access.components(testPaths[i].getPath());
8397

84-
assertArrayEquals("Failure at Index " + i ,expectedComponents[i], components);
98+
assertArrayEquals("Failure at Index " + i, expectedComponents[i], components);
8599
}
86100
}
87101

@@ -96,11 +110,12 @@ protected void testComposeAtLocation(URI uri) {
96110

97111
for (int i = 0; i < testPathComponents.length; ++i) {
98112
testPathComponents[i] = testPathComponents[i].clone();
99-
final URI baseUri = testUris[i].resolve("/");
113+
/* Don't add the "/" if the input uri path is empty. just use it. Otherwise, remove the parts and start with "/" */
114+
final URI baseUri = uri.getPath().isEmpty() ? uri : testUris[i].resolve("/");
100115
final String[] components = testPathComponents[i];
101-
final String stringUriFromComponents = access.compose(baseUri, components);
102-
final URI uriFromComponents = N5URI.getAsUri(stringUriFromComponents);
103-
assertEquals("Failure at Index " + i , testUris[i], uriFromComponents);
116+
final String actualCompose = access.compose(baseUri, components);
117+
final String expectedCompose = access.compose(testUris[i]);
118+
assertEquals("Failure at Index " + i, expectedCompose, actualCompose);
104119
}
105120
}
106121

@@ -111,21 +126,83 @@ public void testComponents() {
111126
}
112127

113128
@Test
114-
public void testComponentsAtRoot() {
129+
public void testComponentsWithPathSlash() {
130+
131+
final URI uriWithPathSlash = setUriPath(tempUri(), "/");
132+
testComponentsAtLocation(uriWithPathSlash);
133+
}
134+
135+
@Test
136+
public void testComponentsWithPathEmpty() {
115137

116-
URI root = tempUri().resolve("/");
117-
testComponentsAtLocation(root);
138+
final URI uriWithPathEmpty = setUriPath(tempUri(), "");
139+
testComponentsAtLocation(uriWithPathEmpty);
118140
}
119141

120142
@Test
121143
public void testCompose() {
122-
testComposeAtLocation(tempUri());
144+
145+
final URI uri = tempUri();
146+
testComposeAtLocation(uri);
147+
148+
final KeyValueAccess kva = newKeyValueAccess();
149+
final URI uriWithPath = setUriPath(uri, "/foo");
150+
assertEquals("Non-empty Path", "/foo", uriWithPath.getPath());
151+
assertComposeEquals("Non-empty Path, no empty or slash in components", kva, uriWithPath, "/foo/bar/baz", "bar", "baz");
152+
assertComposeEquals("Non-empty Path, first components leading slash", kva, uriWithPath, "/bar/baz", "/bar", "baz");
153+
assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/bar/baz", "/", "bar", "baz");
154+
assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/foo/bar/baz", "", "bar", "baz");
155+
assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/bar/baz", "", "/bar", "baz");
156+
assertComposeEquals("Non-empty Path, null and empty inner components", kva, uriWithPath,"/foo/bar/baz", "bar", null, "", "baz");
157+
assertComposeEquals("Non-empty Path, null and empty inner components", kva, uriWithPath,"/bar/baz", "/bar", null, "", "baz");
158+
}
159+
160+
@Test
161+
public void testComposeWithPathSlash() {
162+
163+
final URI uriWithSlashRoot = setUriPath(tempUri(), "/");
164+
assertEquals("Root (/) Path", "/", uriWithSlashRoot.getPath());
165+
testComposeAtLocation(uriWithSlashRoot);
166+
167+
final KeyValueAccess kva = newKeyValueAccess();
168+
assertComposeEquals("Root (/) Path, no empty or slash in components", kva, uriWithSlashRoot, "/bar/baz", "bar", "baz");
169+
assertComposeEquals("Root (/) Path, first components leading slash", kva, uriWithSlashRoot, "/bar/baz", "/bar", "baz");
170+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "/", "bar", "baz");
171+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "", "bar", "baz");
172+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "", "/bar", "baz");
173+
assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithSlashRoot,"/bar/baz", "bar", null, "", "baz");
174+
assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithSlashRoot,"/bar/baz", "/bar", null, "", "baz");
123175
}
124176

125177
@Test
126-
public void testComposeAtRoot() {
178+
public void testComposeWithPathEmpty() {
179+
180+
final URI uriWithEmptyRoot = setUriPath(tempUri(), "");
181+
assertEquals("Empty Path", "", uriWithEmptyRoot.getPath());
182+
testComposeAtLocation(uriWithEmptyRoot);
183+
184+
final KeyValueAccess kva = newKeyValueAccess();
185+
assertComposeEquals("Root (/) Path, no empty or slash in components", kva, uriWithEmptyRoot, "/bar/baz", "bar", "baz");
186+
assertComposeEquals("Root (/) Path, first components leading slash", kva, uriWithEmptyRoot, "/bar/baz", "/bar", "baz");
187+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "/", "bar", "baz");
188+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "", "bar", "baz");
189+
assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "", "/bar", "baz");
190+
assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithEmptyRoot,"/bar/baz", "bar", null, "", "baz");
191+
assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithEmptyRoot,"/bar/baz", "/bar", null, "", "baz");
192+
}
193+
194+
public void assertComposeEquals(String reason, KeyValueAccess kva, URI uri, String absoluteExpectedPath, String... components) {
195+
String actual = kva.compose(uri, components);
196+
String expected = kva.compose(uri.resolve(absoluteExpectedPath));
197+
assertEquals(reason, expected, actual);
198+
}
199+
200+
public URI setUriPath(final URI uri, final String path) {
127201

128-
final URI root = tempUri().resolve("/");
129-
testComposeAtLocation(root);
202+
final URI tempUri = uri.resolve("/");
203+
final String newUri = tempUri.toString().replaceAll(tempUri.getPath() + "$", path);
204+
final URI uriWithNewPath = N5URI.getAsUri(newUri);
205+
assertEquals("setUriPath failed", path, uriWithNewPath.getPath());
206+
return uriWithNewPath;
130207
}
131208
}

src/test/java/org/janelia/saalfeldlab/n5/kva/FileSystemKeyValueAccessTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.io.File;
1010
import java.io.IOException;
1111
import java.net.URI;
12+
import java.net.URISyntaxException;
1213
import java.nio.file.FileSystems;
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
@@ -17,6 +18,8 @@
1718
import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess;
1819
import org.janelia.saalfeldlab.n5.KeyValueAccess;
1920
import org.janelia.saalfeldlab.n5.N5URI;
21+
import org.junit.Ignore;
22+
import org.junit.Test;
2023

2124
/**
2225
* @author Stephan Saalfeld &lt;[email protected]&gt;
@@ -162,4 +165,22 @@ protected void testComposeAtLocation(URI uri) {
162165
assertEquals("Failure at Index " + i , testPath, composedKey);
163166
}
164167
}
168+
169+
@Override
170+
@Test
171+
@Ignore("Empty path is invalid for file URIs.")
172+
public void testComponentsWithPathEmpty() {
173+
/* file URIs are purely paths (optional file: scheme) so empty path resolves to a relative path (not the root of the container).
174+
* Because of that, there is no valid file URI with an empty path (it's just an empty string, which is invalid, or `file://` which is invalid. */
175+
super.testComponentsWithPathEmpty();
176+
}
177+
178+
@Override
179+
@Test
180+
@Ignore("Empty path is invalid for file URIs.")
181+
public void testComposeWithPathEmpty() {
182+
/* file URIs are purely paths (optional file: scheme) so empty path resolves to a relative path (not the root of the container).
183+
* Because of that, there is no valid file URI with an empty path (it's just an empty string, which is invalid, or `file://` which is invalid. */
184+
super.testComposeWithPathEmpty();
185+
}
165186
}

0 commit comments

Comments
 (0)