001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.resources;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.util.ArrayList;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029
030import org.conqat.lib.commons.collections.CollectionUtils;
031import org.conqat.lib.commons.filesystem.FileSystemUtils;
032
033/**
034 * Helper class for listing resources in a given path.
035 */
036public class ResourceUtils {
037
038        /** Extension of class files that should not be considered as a resource. */
039        private static final String CLASS_FILE_EXTENSION = ".class";
040
041        /**
042         * Lists all resources in the given path (non-recursive). Only resources and no
043         * resource folders will be returned.
044         *
045         * @param path
046         *            The path to list resources from. An empty string means the root of
047         *            the contextClass' package.
048         */
049        public static List<Resource> listResources(Class<?> contextClass, String path) {
050                try {
051                        return listResources(contextClass, path, false);
052                } catch (IOException e) {
053                        throw ResourceException.newListFailed(path, contextClass, e);
054                }
055        }
056
057        /**
058         * Lists all resources in the given path (recursively). Only resources and no
059         * resource folders will be returned.
060         * 
061         * @param path
062         *            The path to list resources from. An empty string means the root of
063         *            the contextClass' package.
064         */
065        public static List<Resource> listResourcesRecursively(Class<?> contextClass, String path) {
066                try {
067                        return listResources(contextClass, path, true);
068                } catch (IOException e) {
069                        throw ResourceException.newListFailed(path, contextClass, e);
070                }
071        }
072
073        /**
074         * Gets the path to resources of given context class (relative to
075         * class-resources folder).
076         * 
077         * @param contextClass
078         *            The class for which path to resources should be returned
079         */
080        public static String getPackageResourcePath(Class<?> contextClass) {
081                return contextClass.getPackage().getName().replace('.', FileSystemUtils.UNIX_SEPARATOR);
082        }
083
084        /**
085         * Lists resources in the given path and wraps them into {@link Resource}
086         * objects.
087         */
088        private static List<Resource> listResources(Class<?> contextClass, String path, boolean recursive)
089                        throws IOException {
090                return CollectionUtils.map(listResourceNames(contextClass, path, recursive),
091                                resourcePath -> Resource.of(contextClass, resourcePath));
092        }
093
094        /**
095         * Lists the sub-paths contained in the given path relative to the contextClass.
096         */
097        private static List<String> listResourceNames(Class<?> contextClass, String path, boolean recursive)
098                        throws IOException {
099                Set<URL> resourceContainers = getContainers(contextClass, path);
100                Set<String> resources = new HashSet<>();
101                for (URL containerUrl : resourceContainers) {
102                        List<String> paths = CollectionUtils.filterAndMap(
103                                        FileSystemUtils.listFilesInSameLocationForURL(containerUrl, recursive),
104                                        ResourceUtils::isNoClassFile, subResourcePath -> getResourcePath(path, subResourcePath));
105                        resources.addAll(paths);
106                }
107                return new ArrayList<>(resources);
108        }
109
110        public static Resource getFileBackedResource(Resource resource) {
111                Set<URL> validResourceUrls = getValidAlternativeUrls(resource.getUrl());
112                if (validResourceUrls.isEmpty()) {
113                        return resource;
114                } else {
115                        URL anyUrl = CollectionUtils.getAny(validResourceUrls);
116                        return Resource.of(resource.getContextClass(), resource.getPath(), anyUrl);
117                }
118        }
119
120        /**
121         * Returns all root URLs that could contain content for the package name of the
122         * contextClass. For the distribution this is always the jar file path of the
123         * class, because the resources are packaged together with the class. In a dev
124         * environment the classpath entries for the classes and resources are
125         * different. We therefore guess based on the class' directory where to find the
126         * corresponding test resources. For production resources we currently still
127         * enforce that they result in the same directory as the classes
128         * (ProjectJavaPlugin).
129         */
130        private static Set<URL> getContainers(Class<?> contextClass, String path) throws IOException {
131                URL resourceUrl = contextClass.getResource(path);
132                if (resourceUrl == null) {
133                        throw new FileNotFoundException(path + " does not exist in " + contextClass.getPackage().getName());
134                }
135                Set<URL> resourceContainers = getValidAlternativeUrls(resourceUrl);
136                resourceContainers.add(resourceUrl);
137                return resourceContainers;
138        }
139
140        private static Set<URL> getValidAlternativeUrls(URL resourceUrl) {
141                Set<URL> resourceContainers = new HashSet<>();
142                // Eclipse
143                appendIfValid(resourceUrl, resourceContainers, "/bin/main/", "/class-resources/");
144                appendIfValid(resourceUrl, resourceContainers, "/bin/test/", "/test-data/");
145                appendIfValid(resourceUrl, resourceContainers, "/bin/test/", "/test-utils-data/");
146                appendIfValid(resourceUrl, resourceContainers, "/bin/test/", "/class-resources/");
147
148                // IntelliJ
149                appendIfValid(resourceUrl, resourceContainers, "/out/main/classes/", "/class-resources/");
150                appendIfValid(resourceUrl, resourceContainers, "/out/test/classes/", "/test-data/");
151                appendIfValid(resourceUrl, resourceContainers, "/out/test/classes/", "/test-utils-data/");
152                appendIfValid(resourceUrl, resourceContainers, "/out/test/classes/", "/class-resources/");
153
154                // Gradle
155                appendIfValid(resourceUrl, resourceContainers, "/build/classes/java/main/", "/class-resources/");
156                appendIfValid(resourceUrl, resourceContainers, "/build/classes/java/test/", "/test-data/");
157                appendIfValid(resourceUrl, resourceContainers, "/build/classes/java/test/", "/test-utils-data/");
158                appendIfValid(resourceUrl, resourceContainers, "/build/classes/java/test/", "/class-resources/");
159
160                return resourceContainers;
161        }
162
163        /**
164         * Appends the baseUrl after replacing replacePattern with alternativeDir if it
165         * exists to resourceContainers.
166         */
167        private static void appendIfValid(URL baseUrl, Set<URL> resourceContainers, String replacePattern,
168                        String alternativeDir) {
169                if (!"file".equals(baseUrl.getProtocol()) || !baseUrl.toString().contains(replacePattern)) {
170                        return;
171                }
172                try {
173                        URL url = new URL(baseUrl.toString().replace(replacePattern, alternativeDir));
174                        if (new File(url.toURI()).exists()) {
175                                resourceContainers.add(url);
176                        }
177                } catch (MalformedURLException | URISyntaxException e) {
178                        // Ignore if the directory does not exist
179                }
180        }
181
182        /**
183         * Concatenates path and subResourcePath leaving out path in case it is empty.
184         */
185        private static String getResourcePath(String path, String subResourcePath) {
186                if (!path.isEmpty()) {
187                        return path + FileSystemUtils.UNIX_SEPARATOR + subResourcePath;
188                }
189                return subResourcePath;
190        }
191
192        private static boolean isNoClassFile(String subResourcePath) {
193                return !subResourcePath.endsWith(CLASS_FILE_EXTENSION);
194        }
195}