-
Notifications
You must be signed in to change notification settings - Fork 244
/
HtsPath.java
304 lines (274 loc) · 12.2 KB
/
HtsPath.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package htsjdk.io;
import htsjdk.utils.ValidationUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ProviderNotFoundException;
import java.nio.file.spi.FileSystemProvider;
/**
* Default implementation for IOPath.
*
* This class takes a raw string that is to be interpreted as a path specifier, and converts it internally to a
* URI and/or Path object. If no scheme is provided as part of the raw string used in the constructor(s), the
* input is assumed to represent a file on the local file system, and will be backed by a URI with a "file:/"
* scheme and a path part that is automatically encoded/escaped to ensure it is a valid URI. If the raw string
* contains a scheme, it will be backed by a URI formed from the raw string as presented, with no further
* encoding/escaping.
*
* For example, a URI that contains a scheme and has an embedded "#" in the path will be treated as a URI
* having a fragment delimiter. If the URI contains an scheme, the "#" will be escaped and the encoded "#"
* will be interpreted as part of the URI path.
*
* There are 3 succeeding levels of input validation/conversion:
*
* 1) HtsPath constructor: requires a syntactically valid URI, possibly containing a scheme (if no scheme
* is present the path part will be escaped/encoded), or a valid local file reference
* 2) hasFileSystemProvider: true if the input string is an identifier that is syntactically valid, and is backed by
* an installed {@code java.nio} file system provider that matches the URI scheme
* 3) isPath: syntactically valid URI that can be resolved to a java.io.Path by the associated provider
*
* Definitions taken from RFC 2396 "Uniform Resource Identifiers (URI): Generic Syntax"
* (https://www.ietf.org/rfc/rfc2396.txt):
*
* "absolute" URI - specifies a scheme
* "relative" URI - does not specify a scheme
* "opaque" URI - an "absolute" URI whose scheme-specific part does not begin with a slash character
* "hierarchical" URI - either an "absolute" URI whose scheme-specific part begins with a slash character,
* or a "relative" URI (no scheme)
*
* URIs that do not make use of the slash "/" character for separating hierarchical components are
* considered "opaque" by the generic URI parser.
*
* General syntax for an "absolute" URI:
*
* <scheme>:<scheme-specific-part>
*
* Many "hierarchical" URI schemes use this syntax:
*
* <scheme>://<authority><path>?<query>
*
* More specifically:
*
* absoluteURI = scheme ":" ( hier_part | opaque_part )
* hier_part = ( net_path | abs_path ) [ "?" query ]
* net_path = "//" authority [ abs_path ]
* abs_path = "/" path_segments
* opaque_part = uric_no_slash *uric
* uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
*/
public class HtsPath implements IOPath, Serializable {
private static final long serialVersionUID = 1L;
private final String rawInputString; // raw input string provided by th user; may or may not have a scheme
private final URI uri; // working URI; always has a scheme ("file" if not otherwise specified)
private transient String pathFailureReason; // cache the reason for "toPath" conversion failure
private transient Path cachedPath; // cache the Path associated with this URI if its "Path-able"
/**
* Create an HtsPath from a raw input path string.
*
* If the raw input string already contains a scheme (including a "file" scheme), assume its already
* properly escape/encoded. If no scheme component is present, assume it referencess a raw path on the
* local file system, so try to get a Path first, and then retrieve the URI from the resulting Path.
* This ensures that input strings that are local file references without a scheme component and contain
* embedded characters are valid in file names, but which would otherwise be interpreted as excluded
* URI characters (such as the URI fragment delimiter "#") are properly escape/encoded.
* @param rawInputString a string specifying an input path. May not be null.
*/
public HtsPath(final String rawInputString) {
ValidationUtils.nonNull(rawInputString);
this.rawInputString = rawInputString;
this.uri = getURIForString(rawInputString);
}
/**
* Create an HtsPath from an existing HtsPath.
* @param htsPath an existing PathSpecifier. May not be null.
*/
public HtsPath(final HtsPath htsPath) {
ValidationUtils.nonNull(htsPath);
this.rawInputString = htsPath.getRawInputString();
this.uri = htsPath.getURI();
}
@Override
public URI getURI() {
return uri;
}
@Override
public String getURIString() {
return getURI().toString();
}
/**
* Return the raw input string provided to the constructor.
*/
@Override
public String getRawInputString() { return rawInputString; }
@Override
public boolean hasFileSystemProvider() {
// try to find a provider; assume that our URI always has a scheme
for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
if (provider.getScheme().equalsIgnoreCase(uri.getScheme())) {
return true;
}
}
return false;
}
@Override
public boolean isPath() {
try {
return getCachedPath() != null || toPath() != null;
} catch (ProviderNotFoundException |
FileSystemNotFoundException |
IllegalArgumentException |
AssertionError e) {
// jimfs throws an AssertionError that wraps a URISyntaxException when trying to create path where
// the scheme-specific part is missing or incorrect
pathFailureReason = e.getMessage();
return false;
}
}
/**
* Resolve the URI to a {@link Path} object.
*
* @return the resulting {@code Path}
* @throws RuntimeException if an I/O error occurs when creating the file system
*/
@Override
public Path toPath() {
if (getCachedPath() != null) {
return getCachedPath();
} else {
final Path tmpPath = Paths.get(getURI());
setCachedPath(tmpPath);
return tmpPath;
}
}
@Override
public String getToPathFailureReason() {
if (pathFailureReason == null) {
try {
toPath();
return String.format("'%s' appears to be a valid Path", rawInputString);
} catch (ProviderNotFoundException e) {
return String.format("ProviderNotFoundException: %s", e.getMessage());
} catch (FileSystemNotFoundException e) {
return String.format("FileSystemNotFoundException: %s", e.getMessage());
} catch (IllegalArgumentException e) {
return String.format("IllegalArgumentException: %s", e.getMessage());
} catch (RuntimeException e) {
return String.format("UserException: %s", e.getMessage());
}
}
return pathFailureReason;
}
@Override
public InputStream getInputStream() {
if (!isPath()) {
throw new RuntimeException(getToPathFailureReason());
}
final Path resourcePath = toPath();
try {
return Files.newInputStream(resourcePath);
} catch (IOException e) {
throw new RuntimeException(
String.format("Could not create open input stream for %s (as URI %s)", getRawInputString(), getURIString()), e);
}
}
@Override
public OutputStream getOutputStream() {
if (!isPath()) {
throw new RuntimeException(getToPathFailureReason());
}
final Path resourcePath = toPath();
try {
return Files.newOutputStream(resourcePath);
} catch (IOException e) {
throw new RuntimeException(String.format("Could not open output stream for %s (as URI %s)", getRawInputString(), getURIString()), e);
}
}
// get the cached path associated with this URI if its already been created
protected Path getCachedPath() { return cachedPath; }
protected void setCachedPath(Path path) {
this.cachedPath = path;
}
@Override
public String toString() {
return rawInputString;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof HtsPath)) return false;
HtsPath that = (HtsPath) o;
if (!getRawInputString().equals(that.getRawInputString())) return false;
if (!getURI().equals(that.getURI())) return false;
return true;
}
@Override
public int hashCode() {
int result = getRawInputString().hashCode();
result = 31 * result + getURI().hashCode();
return result;
}
// Called during HtsPath construction to construct and return a URI for the provided input string.
// Has a side-effect of caching a Path object for this PathSpecifier.
private URI getURIForString(final String pathString) {
URI tempURI;
try {
tempURI = new URI(pathString);
if (!tempURI.isAbsolute()) {
// if the URI has no scheme, assume its a local (non-URI) file reference, and resolve
// it to a Path and retrieve the URI from the Path to ensure proper escape/encoding
setCachedPath(Paths.get(pathString));
tempURI = getCachedPath().toUri();
}
} catch (URISyntaxException uriException) {
//check that the uri wasn't a badly encoded absolute uri of some sort
//if you don't do this it will be treated as a badly formed file:// url
assertNoNonFileScheme(pathString, uriException);
// the input string isn't a valid URI; assume its a local (non-URI) file reference, and
// use the URI resulting from the corresponding Path
try {
setCachedPath(Paths.get(pathString));
tempURI = getCachedPath().toUri();
} catch (InvalidPathException | UnsupportedOperationException | SecurityException pathException) {
// we have two exceptions, each of which might be relevant since we can't tell whether
// the user intended to provide a local file reference or a URI, so preserve both
final String errorMessage = String.format(
"%s can't be interpreted as a local file (%s) or as a URI (%s).",
pathString,
pathException.getMessage(),
uriException.getMessage());
throw new IllegalArgumentException(errorMessage, pathException);
}
}
if (!tempURI.isAbsolute()) {
// assert the invariant that every URI we create has a scheme, even if the raw input string does not
throw new RuntimeException("URI has no scheme");
}
return tempURI;
}
/**
* check that there isn't a non file scheme at the start of the path
* @param pathString
* @param cause
*/
private static void assertNoNonFileScheme(String pathString, URISyntaxException cause){
final String[] split = pathString.split(":");
if(split.length > 1){
if(split[0] == null || split[0].isEmpty()){
throw new IllegalArgumentException("Malformed url " + pathString + " includes an empty scheme." +
"\nCheck that it is fully encoded.", cause);
}
if(!split[0].equals("file")){
throw new IllegalArgumentException("Malformed url " + pathString + " includes a scheme: " + split[0] + ":// but was an invalid URI." +
"\nCheck that it is fully encoded.", cause);
}
}
}
}