3131import java .nio .file .FileSystem ;
3232import java .nio .file .FileSystemAlreadyExistsException ;
3333import java .nio .file .FileSystemNotFoundException ;
34+ import java .nio .file .Files ;
3435import java .nio .file .LinkOption ;
3536import java .nio .file .NoSuchFileException ;
3637import java .nio .file .NotDirectoryException ;
4647import java .time .Duration ;
4748import java .util .ArrayList ;
4849import java .util .Arrays ;
50+ import java .util .Collections ;
4951import java .util .HashMap ;
52+ import java .util .HashSet ;
5053import java .util .List ;
5154import java .util .Map ;
55+ import java .util .Objects ;
5256import java .util .Set ;
5357import java .util .concurrent .ConcurrentHashMap ;
5458import java .util .concurrent .ConcurrentMap ;
@@ -158,6 +162,17 @@ public final class AzureFileSystemProvider extends FileSystemProvider {
158162
159163 private static final String ACCOUNT_QUERY_KEY = "account" ;
160164 private static final int COPY_TIMEOUT_SECONDS = 30 ;
165+ private static final Set <OpenOption > OUTPUT_STREAM_DEFAULT_OPTIONS =
166+ Collections .unmodifiableSet (new HashSet <>(Arrays .asList (StandardOpenOption .CREATE ,
167+ StandardOpenOption .WRITE ,
168+ StandardOpenOption .TRUNCATE_EXISTING )));
169+ private static final Set <OpenOption > OUTPUT_STREAM_SUPPORTED_OPTIONS =
170+ Collections .unmodifiableSet (new HashSet <>(Arrays .asList (
171+ StandardOpenOption .CREATE_NEW ,
172+ StandardOpenOption .CREATE ,
173+ StandardOpenOption .WRITE ,
174+ // Though we don't actually truncate, the same result is achieved by overwriting the destination.
175+ StandardOpenOption .TRUNCATE_EXISTING )));
161176
162177 private final ConcurrentMap <String , FileSystem > openFileSystems ;
163178
@@ -252,12 +267,20 @@ public Path getPath(URI uri) {
252267 }
253268
254269 /**
255- * Unsupported. Use {@link #newInputStream(Path, OpenOption...)} or {@link #newOutputStream(Path, OpenOption...)}
256- * instead.
270+ * Opens or creates a file, returning a seekable byte channel to access the file.
271+ * <p>
272+ * This method is primarily offered to support some jdk convenience methods such as
273+ * {@link Files#createFile(Path, FileAttribute[])} which requires opening a channel and closing it. A channel may
274+ * only be opened in read mode OR write mode. It may not be opened in read/write mode. Seeking is supported for
275+ * reads, but not for writes. Modifications to existing files is not permitted--only creating new files or
276+ * overwriting existing files.
277+ * <p>
278+ * This type is not threadsafe to prevent having to hold locks across network calls.
279+ * <p>
257280 *
258- * @param path the Path
259- * @param set open options
260- * @param fileAttributes attributes
281+ * @param path the path of the file to open
282+ * @param set options specifying how the file should be opened
283+ * @param fileAttributes an optional list of file attributes to set atomically when creating the directory
261284 * @return a new seekable byte channel
262285 * @throws UnsupportedOperationException Operation is not supported.
263286 * @throws IllegalArgumentException if the set contains an invalid combination of options
@@ -269,7 +292,17 @@ public Path getPath(URI uri) {
269292 @ Override
270293 public SeekableByteChannel newByteChannel (Path path , Set <? extends OpenOption > set ,
271294 FileAttribute <?>... fileAttributes ) throws IOException {
272- throw LoggingUtility .logError (logger , new UnsupportedOperationException ());
295+ if (Objects .isNull (set )) {
296+ set = Collections .emptySet ();
297+ }
298+
299+ if (set .contains (StandardOpenOption .WRITE )) {
300+ return new AzureSeekableByteChannel (
301+ (NioBlobOutputStream ) this .newOutputStreamInternal (path , set , fileAttributes ), path );
302+ } else {
303+ return new AzureSeekableByteChannel (
304+ (NioBlobInputStream ) this .newInputStream (path , set .toArray (new OpenOption [0 ])), path );
305+ }
273306 }
274307
275308 /**
@@ -349,24 +382,19 @@ public InputStream newInputStream(Path path, OpenOption... options) throws IOExc
349382 */
350383 @ Override
351384 public OutputStream newOutputStream (Path path , OpenOption ... options ) throws IOException {
385+ return newOutputStreamInternal (path , new HashSet <>(Arrays .asList (options )));
386+ }
387+
388+ OutputStream newOutputStreamInternal (Path path , Set <? extends OpenOption > optionsSet ,
389+ FileAttribute <?>... fileAttributes ) throws IOException {
352390 // If options are empty, add Create, Write, TruncateExisting as defaults per nio docs.
353- if (options == null || options .length == 0 ) {
354- options = new OpenOption [] {
355- StandardOpenOption .CREATE ,
356- StandardOpenOption .WRITE ,
357- StandardOpenOption .TRUNCATE_EXISTING };
391+ if (optionsSet == null || optionsSet .size () == 0 ) {
392+ optionsSet = OUTPUT_STREAM_DEFAULT_OPTIONS ;
358393 }
359- List <OpenOption > optionsList = Arrays .asList (options );
360394
361395 // Check for unsupported options.
362- List <OpenOption > supportedOptions = Arrays .asList (
363- StandardOpenOption .CREATE_NEW ,
364- StandardOpenOption .CREATE ,
365- StandardOpenOption .WRITE ,
366- // Though we don't actually truncate, the same result is achieved by overwriting the destination.
367- StandardOpenOption .TRUNCATE_EXISTING );
368- for (OpenOption option : optionsList ) {
369- if (!supportedOptions .contains (option )) {
396+ for (OpenOption option : optionsSet ) {
397+ if (!OUTPUT_STREAM_SUPPORTED_OPTIONS .contains (option )) {
370398 throw LoggingUtility .logError (logger , new UnsupportedOperationException ("Unsupported option: "
371399 + option .toString ()));
372400 }
@@ -376,9 +404,9 @@ public OutputStream newOutputStream(Path path, OpenOption... options) throws IOE
376404 Write must be specified. Either create_new or truncate must be specified. This is to ensure that no edits or
377405 appends are allowed.
378406 */
379- if (!optionsList .contains (StandardOpenOption .WRITE )
380- || !(optionsList .contains (StandardOpenOption .TRUNCATE_EXISTING )
381- || optionsList .contains (StandardOpenOption .CREATE_NEW ))) {
407+ if (!optionsSet .contains (StandardOpenOption .WRITE )
408+ || !(optionsSet .contains (StandardOpenOption .TRUNCATE_EXISTING )
409+ || optionsSet .contains (StandardOpenOption .CREATE_NEW ))) {
382410 throw LoggingUtility .logError (logger ,
383411 new IllegalArgumentException ("Write and either CreateNew or TruncateExisting must be specified to open "
384412 + "an OutputStream" ));
@@ -396,14 +424,14 @@ public OutputStream newOutputStream(Path path, OpenOption... options) throws IOE
396424
397425 // Writing to an empty location requires a create option.
398426 if (status .equals (DirectoryStatus .DOES_NOT_EXIST )
399- && !(optionsList .contains (StandardOpenOption .CREATE )
400- || optionsList .contains (StandardOpenOption .CREATE_NEW ))) {
427+ && !(optionsSet .contains (StandardOpenOption .CREATE )
428+ || optionsSet .contains (StandardOpenOption .CREATE_NEW ))) {
401429 throw LoggingUtility .logError (logger , new IOException ("Writing to an empty location requires a create "
402430 + "option. Path: " + path .toString ()));
403431 }
404432
405433 // Cannot write to an existing file if create new was specified.
406- if (status .equals (DirectoryStatus .NOT_A_DIRECTORY ) && optionsList .contains (StandardOpenOption .CREATE_NEW )) {
434+ if (status .equals (DirectoryStatus .NOT_A_DIRECTORY ) && optionsSet .contains (StandardOpenOption .CREATE_NEW )) {
407435 throw LoggingUtility .logError (logger , new IOException ("A file already exists at this location and "
408436 + "CREATE_NEW was specified. Path: " + path .toString ()));
409437 }
@@ -417,12 +445,17 @@ public OutputStream newOutputStream(Path path, OpenOption... options) throws IOE
417445
418446 // Add an extra etag check for create new
419447 BlobRequestConditions rq = null ;
420- if (optionsList .contains (StandardOpenOption .CREATE_NEW )) {
448+ if (optionsSet .contains (StandardOpenOption .CREATE_NEW )) {
421449 rq = new BlobRequestConditions ().setIfNoneMatch ("*" );
422450 }
423451
424- return new NioBlobOutputStream (resource .getBlobClient ().getBlockBlobClient ().getBlobOutputStream (pto , null ,
425- null , null , rq ), resource .getPath ());
452+ // For parsing properties and metadata
453+ if (fileAttributes == null ) {
454+ fileAttributes = new FileAttribute <?>[0 ];
455+ }
456+ resource .setFileAttributes (Arrays .asList (fileAttributes ));
457+
458+ return new NioBlobOutputStream (resource .getBlobOutputStream (pto , rq ), resource .getPath ());
426459 }
427460
428461 /**
@@ -992,7 +1025,6 @@ public Map<String, Object> readAttributes(Path path, String attributes, LinkOpti
9921025
9931026 // If "*" is specified, add all of the attributes from the specified set.
9941027 if (attributeName .equals ("*" )) {
995- Set <String > attributesToAdd ;
9961028 if (viewType .equals (AzureBasicFileAttributeView .NAME )) {
9971029 for (String attr : AzureBasicFileAttributes .ATTRIBUTE_STRINGS ) {
9981030 results .put (attr , attributeSuppliers .get (attr ).get ());
0 commit comments