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}