001package org.conqat.lib.commons.resources; 002 003import static org.conqat.lib.commons.filesystem.FileSystemUtils.ensureParentDirectoryExists; 004import static org.conqat.lib.commons.filesystem.FileSystemUtils.readStreamUTF8; 005 006import java.io.File; 007import java.io.FileOutputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.URISyntaxException; 011import java.net.URL; 012import java.net.URLClassLoader; 013import java.nio.file.FileSystemNotFoundException; 014import java.nio.file.Files; 015import java.nio.file.Paths; 016import java.util.Arrays; 017import java.util.List; 018import java.util.Objects; 019import java.util.Optional; 020 021import org.conqat.lib.commons.filesystem.FileSystemUtils; 022import org.conqat.lib.commons.string.StringUtils; 023 024/** 025 * Represents a java resource which is identified by a {@link #contextClass} and 026 * a {@link #path}. The resource will be loaded using the same class loader that 027 * was used to load the {@link #contextClass}. The path consists of the class' 028 * package name plus the {@link #path}. A resource object is always guaranteed 029 * to exist meaning that instantiation of a non-existent resource will fail. 030 */ 031public class Resource implements Comparable<Resource> { 032 033 /** Character that separates a resource' basename from the extension. */ 034 private static final char RESOURCE_EXTENSION_SEPARATOR = '.'; 035 036 /** 037 * The context class. The resource is expected below the class's package name 038 * and will be loaded using the same class loader that was used to load the 039 * class. 040 */ 041 private final Class<?> contextClass; 042 043 /** 044 * A path including the resource name and extension relative to the package name 045 * of the #contextClass. The path is separated by forward slashes on all 046 * operating systems. The path must not navigate up with "..". 047 */ 048 private final String path; 049 050 /** The resolved url of the resource. */ 051 private final URL url; 052 053 /** 054 * Creates an instance of a resource. If the resource does not exist and 055 * AssertionError is thrown. Check with {@link #exists(Class, String)} before 056 * creating an instance in case it might be missing. 057 */ 058 public static Resource of(Class<?> contextClass, String path) { 059 URL resourceUrl = contextClass.getResource(path); 060 boolean exists = resourceUrl != null; 061 if (!exists) { 062 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 063 String message = getAbsolutePath(contextClass, path) + " does not exist!"; 064 if (contextClassLoader instanceof URLClassLoader) { 065 message += " Classpath: " + Arrays.toString(((URLClassLoader) contextClassLoader).getURLs()); 066 } else { 067 message += " Context class loader is of type " + contextClassLoader.getClass(); 068 } 069 throw new AssertionError(message); 070 } 071 return new Resource(contextClass, path, resourceUrl); 072 } 073 074 /** 075 * Creates an instance of a resource. Returns empty if the resource does not 076 * exist. 077 */ 078 public static Optional<Resource> asOptional(Class<?> contextClass, String path) { 079 URL resourceUrl = contextClass.getResource(path); 080 if (resourceUrl == null) { 081 return Optional.empty(); 082 } 083 return Optional.of(new Resource(contextClass, path, resourceUrl)); 084 } 085 086 /** 087 * Creates an instance of a resource. If the resource does not exist an 088 * AssertionError is thrown. Check with {@link #exists(Class, String)} before 089 * creating an instance in case it might be missing. 090 */ 091 public static Resource of(Class<?> contextClass, String path, URL url) { 092 return new Resource(contextClass, path, url); 093 } 094 095 /** 096 * Returns the resource's absolute path including the {@link #contextClass}' 097 * package name. 098 */ 099 public static String getAbsolutePath(Class<?> contextClass, String path) { 100 if (path.startsWith(String.valueOf(FileSystemUtils.UNIX_SEPARATOR))) { 101 // strip leading slash 102 return path.substring(1); 103 } 104 return ResourceUtils.getPackageResourcePath(contextClass) + FileSystemUtils.UNIX_SEPARATOR + path; 105 } 106 107 /** 108 * Returns true is the resource exists. 109 */ 110 public static boolean exists(Class<?> contextClass, String path) { 111 return contextClass.getResource(path) != null; 112 } 113 114 private Resource(Class<?> contextClass, String path, URL url) { 115 this.path = path; 116 this.contextClass = contextClass; 117 this.url = url; 118 } 119 120 /** Returns the name of the resource including the extension. */ 121 public String getName() { 122 return FileSystemUtils.getLastPathSegment(getPath()); 123 } 124 125 /** Returns the resource's base name without extension. */ 126 public String getBaseName() { 127 return StringUtils.removeLastPart(getName(), RESOURCE_EXTENSION_SEPARATOR); 128 } 129 130 /** Returns the resource's extension. */ 131 public String getExtension() { 132 return StringUtils.getLastPart(getName(), RESOURCE_EXTENSION_SEPARATOR); 133 } 134 135 /** 136 * Returns true is the resource's extension matches one of the given extensions. 137 * In contrast to {@link #getExtension()} this method supports also extensions 138 * that contain dots. 139 */ 140 public boolean hasExtension(String... extensions) { 141 return Arrays.stream(extensions) 142 .anyMatch(extension -> getName().endsWith(RESOURCE_EXTENSION_SEPARATOR + extension)); 143 } 144 145 /** Returns the resource's context class. */ 146 public Class<?> getContextClass() { 147 return contextClass; 148 } 149 150 /** 151 * Returns the resource's path relative to the {@link #contextClass} including 152 * the resource's name. 153 */ 154 public String getPath() { 155 return path; 156 } 157 158 /** Returns the resource's url. */ 159 public URL getUrl() { 160 return url; 161 } 162 163 /** 164 * Returns the resource's parent path. This is the {@link #getPath()} without 165 * the resource's name. 166 */ 167 public String getParentPath() { 168 return StringUtils.stripSuffix(getPath(), getName()); 169 } 170 171 /** 172 * Returns the resource's absolute path including the {@link #contextClass}' 173 * package name. 174 */ 175 public String getAbsolutePath() { 176 return getAbsolutePath(contextClass, path); 177 } 178 179 /** 180 * Returns only a single path segment of the resource's path. If the index is 181 * negative the path segments are counted from the end of the path. E.g. for 182 * "base/sub/name.txt" 0=base, -1=name.txt, -2=1=sub. 183 */ 184 public String getPathSegmentAt(int index) { 185 String[] pathSegments = FileSystemUtils.getPathSegments(path); 186 if (index < 0) { 187 return pathSegments[pathSegments.length + index]; 188 } 189 return pathSegments[index]; 190 } 191 192 /** 193 * Get resource as stream. 194 */ 195 public InputStream getAsStream() { 196 try { 197 return url.openStream(); 198 } catch (IOException e) { 199 throw ResourceException.newLoadFailed(this, e); 200 } 201 } 202 203 /** 204 * Returns the resource as a raw byte array. 205 */ 206 public byte[] getAsByteArray() { 207 try { 208 return FileSystemUtils.readStreamBinary(getAsStream()); 209 } catch (IOException e) { 210 throw ResourceException.newLoadFailed(this, e); 211 } 212 } 213 214 /** 215 * Get resource as string (assuming UTF-8 encoding) without normalized line 216 * separators. 217 */ 218 public String getContentAsRawString() { 219 try { 220 return readStreamUTF8(getAsStream()); 221 } catch (IOException e) { 222 throw ResourceException.newLoadFailed(this, e); 223 } 224 } 225 226 /** 227 * Get resource as string (assuming UTF-8 encoding) with normalized line 228 * separators. 229 */ 230 public String getContent() { 231 return StringUtils.normalizeLineSeparatorsPlatformIndependent(getContentAsRawString()); 232 } 233 234 /** 235 * Returns the content of the resource split into lines. 236 */ 237 public List<String> getLines() { 238 return StringUtils.splitLinesAsList(getContentAsRawString()); 239 } 240 241 /** 242 * Creates a temporary copy of the resource in the systems default temp file 243 * location. The file will be marked for deletion when the JVM shuts down. 244 */ 245 public File getAsTmpFile() throws IOException { 246 File tempFile = File.createTempFile("test-data-", getName()); 247 copyTo(tempFile); 248 tempFile.deleteOnExit(); 249 return tempFile; 250 } 251 252 /** 253 * Copies the resource to the given file. Parent directories are created if they 254 * do not exist yet. If the file itself already exists it wil be overridden. 255 */ 256 public File copyTo(File file) throws IOException { 257 ensureParentDirectoryExists(file); 258 try (FileOutputStream output = new FileOutputStream(file); InputStream input = getAsStream()) { 259 FileSystemUtils.copy(input, output); 260 } 261 return file; 262 } 263 264 /** 265 * Unzips the resource into into the given folder under a folder with the same 266 * name as the resource's base name. 267 */ 268 public void unzipTo(File tempDir) throws IOException { 269 FileSystemUtils.unzip(getAsStream(), tempDir); 270 } 271 272 @Override 273 public int compareTo(Resource other) { 274 return getAbsolutePath().compareTo(other.getAbsolutePath()); 275 } 276 277 @Override 278 public String toString() { 279 return getName(); 280 } 281 282 @Override 283 public boolean equals(Object o) { 284 if (this == o) { 285 return true; 286 } 287 if (o == null || getClass() != o.getClass()) { 288 return false; 289 } 290 Resource resource = (Resource) o; 291 return getAbsolutePath().equals(resource.getAbsolutePath()); 292 } 293 294 @Override 295 public int hashCode() { 296 return Objects.hash(getAbsolutePath()); 297 } 298 299 /** 300 * Returns the timestamp in milliseconds of the last modification in case the 301 * underlying resource url is a file. Returns -1 of the underlying resource is 302 * an entry in a jar file. Calling this method only makes sense after calling 303 * {@link ResourceUtils#getFileBackedResource(Resource)}. 304 */ 305 public long lastModified() { 306 try { 307 return Files.getLastModifiedTime(Paths.get(url.toURI())).toMillis(); 308 } catch (IOException | URISyntaxException | FileSystemNotFoundException e) { 309 return -1; 310 } 311 } 312}