001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.common.io.resource; 019 020import com.google.common.io.ByteSink; 021import com.google.common.io.ByteSource; 022import com.google.common.io.MoreFiles; 023import com.google.common.net.MediaType; 024 025import dev.enola.common.io.MoreFileSystems; 026import dev.enola.common.io.iri.URIs; 027 028import org.jspecify.annotations.Nullable; 029 030import java.io.File; 031import java.io.IOException; 032import java.net.URI; 033import java.nio.file.*; 034import java.util.Arrays; 035 036/** 037 * {@link Resource} for a file (not a directory) at a {@link Path} on a {@link FileSystem}. 038 * 039 * <p>Note that a {@link Path} object instance is associated to a FileSystem, see {@link 040 * Path#getFileSystem()}. But in its text-form ({@link Path#of(String, String...)} and {@link 041 * Path#toString()}) this association is lost, and it should thus be avoided. In its URI-form 042 * ({@link Path#of(URI)} and {@link Path#toUri()}) this is preserved; typically via the "scheme" 043 * (and possibly the "authority") component/s of an URI. So a path in text does not identify a 044 * filesystem, whereas an URI can. While the "file:" scheme is obviously the most well-known and 045 * common one, the JVM actually can and does support others, notably the "jar:file:" one. To avoid 046 * related confusions, this class intentionally only offers constructors which take {@link URI} 047 * instead of {@link Path} (or exposing {@link File}) arguments. 048 */ 049public class FileResource extends BaseResource implements Resource { 050 // TODO Rename FileResource to FileSystemPathResource! 051 052 public static class Provider implements ResourceProvider { 053 054 private final @Nullable File baseFile; 055 056 public Provider(File baseFile) { 057 this.baseFile = baseFile; 058 } 059 060 public Provider() { 061 this.baseFile = null; 062 } 063 064 @Override 065 public Resource getResource(URI uri) { 066 // URIs like "jar:file:/tmp/libmodels.jar!/enola.dev/properties.ttl" are not supported 067 // here. The UrlResource accepts jar: scheme though - and can read (but not write) them. 068 if ("jar".equals(uri.getScheme())) return null; 069 070 if (MoreFileSystems.URI_SCHEMAS.contains(uri.getScheme()) 071 && isValid(URIs.getFilePath(uri))) return new FileResource(uri); 072 073 // NB: There's very similar logic in ClasspathResource 074 if (uri.getScheme() == null && baseFile != null) { 075 if (uri.toString().contains("..")) 076 throw new IllegalArgumentException(uri.toString()); 077 return new ReadableButNotWritableDelegatingResource( 078 new FileResource(new File(baseFile, uri.toString()).toURI())); 079 } 080 081 return null; 082 } 083 } 084 085 private static final OpenOption[] EMPTY_OPTIONS = new OpenOption[0]; 086 087 private final Path path; 088 private final OpenOption[] openOptions; 089 090 public FileResource(URI uri, MediaType mediaType, OpenOption... openOptions) { 091 super(uri, mediaType); 092 this.path = checkValid(URIs.getFilePath(uri)); 093 this.openOptions = safe(openOptions); 094 } 095 096 public FileResource(URI uri, OpenOption... openOptions) { 097 super(uri, MoreFiles.asByteSource(URIs.getFilePath(uri), openOptions)); 098 this.path = checkValid(URIs.getFilePath(uri)); 099 this.openOptions = safe(openOptions); 100 } 101 102 private static Path checkValid(Path path) { 103 if (isValid(path)) return path; 104 throw new IllegalArgumentException( 105 "Exists, but is a directory; thus cannot be a Resource: " + path); 106 } 107 108 private static boolean isValid(Path path) { 109 if (Files.notExists(path)) return true; 110 return !Files.isDirectory(path); 111 } 112 113 private static OpenOption[] safe(OpenOption[] openOptions) { 114 if (openOptions.length == 0) return EMPTY_OPTIONS; // skipcq: JAVA-S1049 115 else return Arrays.copyOf(openOptions, openOptions.length); 116 } 117 118 @Override 119 public ByteSink byteSink() { 120 var parentDirectoryPath = path.getParent(); 121 if (parentDirectoryPath != null) { 122 try { 123 if (!Files.exists(parentDirectoryPath)) 124 Files.createDirectories(parentDirectoryPath); 125 } catch (IOException e) { 126 return new ErrorByteSink(e); 127 } 128 } 129 return MoreFiles.asByteSink(path, openOptions); 130 } 131 132 @Override 133 public ByteSource byteSource() { 134 return MoreFiles.asByteSource(path, openOptions); 135 } 136}