View Javadoc
1   /**
2    * This file Copyright (c) 2016-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.resourceloader.classpath;
35  
36  import info.magnolia.init.MagnoliaConfigurationProperties;
37  import info.magnolia.resourceloader.classpath.service.ClasspathServiceConfiguration;
38  
39  import java.net.URL;
40  import java.net.URLClassLoader;
41  import java.nio.charset.Charset;
42  import java.nio.charset.StandardCharsets;
43  import java.util.Collection;
44  import java.util.List;
45  import java.util.function.Predicate;
46  import java.util.regex.Pattern;
47  import java.util.stream.Collectors;
48  
49  import javax.inject.Inject;
50  import javax.inject.Singleton;
51  import javax.servlet.ServletContext;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.reflections.util.ClasspathHelper;
55  import org.reflections.util.FilterBuilder;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import com.google.common.base.Joiner;
60  import com.google.common.base.Predicates;
61  import com.google.common.collect.Lists;
62  
63  import lombok.Value;
64  
65  /**
66   * Default {@link info.magnolia.resourceloader.classpath.service.ClasspathService classpath service} configurations aggregated in one place.
67   *
68   * @see #developmentMode()
69   * @see #productionMode()
70   * @see #legacy()
71   */
72  @Singleton
73  public class DefaultClasspathServiceConfigurations {
74  
75      private static final Logger log = LoggerFactory.getLogger(DefaultClasspathServiceConfigurations.class);
76  
77      public static final String LEGACY_PREFIX = "mgnl-resources";
78      private static final String PREFIX_AND_SLASH = LEGACY_PREFIX + "/";
79      private static final String PREFIX_AND_DOT = LEGACY_PREFIX + ".";
80  
81      private final ServletContext servletContext;
82      private final MagnoliaConfigurationProperties configurationProperties;
83  
84      @Inject
85      public DefaultClasspathServiceConfigurations(ServletContext servletContext, MagnoliaConfigurationProperties configurationProperties) {
86          this.servletContext = servletContext;
87          this.configurationProperties = configurationProperties;
88      }
89  
90      public Builder developmentMode() {
91          return defaults();
92      }
93  
94      public Builder legacy() {
95          return productionMode().
96                  withResourceFilter(new StartsWith(Lists.newArrayList(PREFIX_AND_DOT, PREFIX_AND_SLASH)));
97      }
98  
99      public Builder productionMode() {
100         return defaults();
101     }
102 
103     public Builder defaults() {
104         return new Builder().
105                 withCharset(StandardCharsets.UTF_8).
106                 withScannableResourcePattern(defaultScannableResourcePattern()).
107                 withResourceFilter(defaultResourceFilter()).
108                 withClasspathLocations(defaultClasspathLocations());
109     }
110 
111     public final Pattern defaultScannableResourcePattern() {
112         String pattern = configurationProperties.getProperty("magnolia.resources.classpath.observation.pattern");
113         if (StringUtils.isNotBlank(pattern)) {
114             return Pattern.compile(pattern);
115         } else {
116             log.debug("Classpath observation pattern is not being set explicitly, current observation is made for only ftl + yaml files");
117             return Pattern.compile(".*\\.(ftl|yaml)$");
118         }
119     }
120 
121     public final Predicate<String> defaultResourceFilter() {
122         final FilterBuilder filterBuilder = new FilterBuilder();
123         return applyResourceExclusions(filterBuilder);
124     }
125 
126     public final Iterable<URL> defaultClasspathLocations() {
127         final ClassLoader contextClassLoader = ClasspathHelper.contextClassLoader();
128         final Collection<URL> allURLs;
129         if (contextClassLoader instanceof URLClassLoader) {
130             allURLs = ClasspathHelper.forClassLoader(contextClassLoader);
131         } else {
132             // Some app servers (e.g. JBoss EAP/Wildfly) have complex classloader hierarchies and the context classloader
133             // does not provide the resource search path URLs. For such a case in order to make sure we still get the relevant
134             // URLs from the WEB-INF/lib we fall back to resolving them via a servlet context.
135             allURLs = ClasspathHelper.forWebInfLib(servletContext);
136         }
137         return allURLs.stream()
138                 .filter(urlsFilter())
139                 .collect(Collectors.toList());
140     }
141 
142     /**
143      * Builder class for {@link ClasspathServiceConfiguration} objects.
144      */
145     public static final class Builder {
146         private Iterable<URL> classpathLocations;
147         private Predicate<String> resourceFilter;
148         private Charset charset;
149         private Pattern scannableResourcePattern;
150 
151         public Builder withClasspathLocations(Iterable<URL> classpathLocations) {
152             this.classpathLocations = classpathLocations;
153             return this;
154         }
155 
156         public Builder withResourceFilter(Predicate<String> resourceFilter) {
157             this.resourceFilter = resourceFilter;
158             return this;
159         }
160 
161         public Builder withCharset(Charset charset) {
162             this.charset = charset;
163             return this;
164         }
165 
166         public Builder withScannableResourcePattern(Pattern scannableResourcePattern) {
167             this.scannableResourcePattern = scannableResourcePattern;
168             return this;
169         }
170 
171         public ClasspathServiceConfiguration build() {
172             return new ConfiguredClasspathServiceConfiguration(classpathLocations, resourceFilter, charset, scannableResourcePattern);
173         }
174 
175         @Value
176         private static class ConfiguredClasspathServiceConfiguration implements ClasspathServiceConfiguration {
177             private Iterable<URL> classpathLocations;
178             private Predicate<String> resourceFilter;
179             private Charset charset;
180             private Pattern monitoredResourcePattern;
181         }
182     }
183 
184     private Predicate<? super URL> urlsFilter() {
185         final String excludedUrlExtensionsPattern = extensionsPattern(excludedUrlExtensions());
186         return URLPredicate.excludedExtensions(excludedUrlExtensionsPattern);
187 
188     }
189 
190     private String extensionsPattern(String[] extensions) {
191         return ".*\\.(" + Joiner.on('|').join(extensions) + ")$";
192     }
193 
194     private String[] excludedUrlExtensions() {
195         return new String[]{
196                 // native libs
197                 "dylib", "dll", "so", "jnilib",
198                 "class",
199                 // some people end up with .pom files on their classpaths
200                 "pom"
201         };
202     }
203 
204     private FilterBuilder applyResourceExclusions(FilterBuilder filterBuilder) {
205         // "mgnl-resources" is re-included by LegacyClasspathResourceOrigin, we don't want any other mgnl-* folder for now except for mgnl-i18n.
206         filterBuilder.exclude("mgnl-bootstrap.*"); // exclude all folders starting with mgnl-bootstrap
207         filterBuilder.exclude("mgnl-resources.*"); // exclude mgnl-resources* (reflections uses package notation, not folders)
208         filterBuilder.exclude("mgnl-nodetypes.*"); // ditto for mgnl-i18n
209         filterBuilder.exclude("mgnl-files.*"); // ditto for mgnl-files
210         filterBuilder.include("mgnl-i18n.*"); // ditto for mgnl-i18n
211 
212         filterBuilder.exclude(extensionsPattern(excludedResourcesExtensions()));
213 
214         final String[] excludedPackages = excludedPackages();
215         for (String excludedPackage : excludedPackages) {
216             filterBuilder.excludePackage(excludedPackage);
217         }
218         return filterBuilder;
219     }
220 
221     private String[] excludedResourcesExtensions() {
222         return new String[]{
223                 "package\\.html", "java", "jar",
224                 // native libs (quite possibly covered by the com.* excludes but eh)
225                 "dylib", "dll", "jnilib", "class"
226         };
227     }
228 
229     private String[] excludedPackages() {
230         return new String[]{
231                 "META-INF",
232                 "com.oracle.java",
233                 "com.oracle.tools",
234                 "com.sun",
235                 "sun",
236                 "oracle", // Only found /oracle/jrockit/jfr/settings/jfc.xsd in my tests but eh
237                 "java",
238                 "javax",
239                 "jdk",
240                 "org.apache", // TODO is this too strict or arbitrary ?
241                 "lombok",
242                 "VAADIN",
243                 "gwt-unitCache"
244         };
245     }
246 
247     /**
248      * A predicate that matches a URL's toString() against a Matcher<String>.
249      */
250     private static class URLPredicate implements Predicate<URL> {
251 
252         public static Predicate<? super URL> excludedExtensions(String excludedUrlExtensionsPattern) {
253             final Predicate<CharSequence> exclude = Predicates.containsPattern(excludedUrlExtensionsPattern).negate();
254             return new URLPredicate(exclude);
255         }
256 
257         private final Predicate<CharSequence> predicate;
258 
259         protected URLPredicate(Predicate<CharSequence> predicate) {
260             this.predicate = predicate;
261         }
262 
263         @Override
264         public boolean test(URL input) {
265             return predicate.test(input.toString());
266         }
267     }
268 
269     private static class StartsWith implements Predicate<String> {
270 
271         private final List<String> prefixes;
272 
273         public StartsWith(List<String> prefixes) {
274             this.prefixes = prefixes;
275         }
276 
277         @Override
278         public boolean test(String input) {
279             for (String prefix : prefixes) {
280                 if (input.startsWith(prefix)) {
281                     return true;
282                 }
283             }
284             return false;
285         }
286     }
287 }