View Javadoc
1   /**
2    * This file Copyright (c) 2016-2017 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.regex.Pattern;
46  
47  import javax.inject.Inject;
48  import javax.inject.Singleton;
49  import javax.servlet.ServletContext;
50  
51  import org.apache.commons.lang3.StringUtils;
52  import org.reflections.util.ClasspathHelper;
53  import org.reflections.util.FilterBuilder;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  import com.google.common.base.Joiner;
58  import com.google.common.base.Predicate;
59  import com.google.common.base.Predicates;
60  import com.google.common.collect.Collections2;
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 Collections2.filter(allURLs, urlsFilter());
138     }
139 
140     /**
141      * Builder class for {@link ClasspathServiceConfiguration} objects.
142      */
143     public static final class Builder {
144         private Iterable<URL> classpathLocations;
145         private Predicate<String> resourceFilter;
146         private Charset charset;
147         private Pattern scannableResourcePattern;
148 
149         public Builder withClasspathLocations(Iterable<URL> classpathLocations) {
150             this.classpathLocations = classpathLocations;
151             return this;
152         }
153 
154         public Builder withResourceFilter(Predicate<String> resourceFilter) {
155             this.resourceFilter = resourceFilter;
156             return this;
157         }
158 
159         public Builder withCharset(Charset charset) {
160             this.charset = charset;
161             return this;
162         }
163 
164         public Builder withScannableResourcePattern(Pattern scannableResourcePattern) {
165             this.scannableResourcePattern = scannableResourcePattern;
166             return this;
167         }
168 
169         public ClasspathServiceConfiguration build() {
170             return new ConfiguredClasspathServiceConfiguration(classpathLocations, resourceFilter, charset, scannableResourcePattern);
171         }
172 
173         @Value
174         private static class ConfiguredClasspathServiceConfiguration implements ClasspathServiceConfiguration {
175             private Iterable<URL> classpathLocations;
176             private Predicate<String> resourceFilter;
177             private Charset charset;
178             private Pattern monitoredResourcePattern;
179         }
180     }
181 
182     private Predicate<? super URL> urlsFilter() {
183         final String excludedUrlExtensionsPattern = extensionsPattern(excludedUrlExtensions());
184         return URLPredicate.excludedExtensions(excludedUrlExtensionsPattern);
185 
186     }
187 
188     private String extensionsPattern(String[] extensions) {
189         return ".*\\.(" + Joiner.on('|').join(extensions) + ")$";
190     }
191 
192     private String[] excludedUrlExtensions() {
193         return new String[]{
194                 // native libs
195                 "dylib", "dll", "so", "jnilib",
196                 "class",
197                 // some people end up with .pom files on their classpaths
198                 "pom"
199         };
200     }
201 
202     private FilterBuilder applyResourceExclusions(FilterBuilder filterBuilder) {
203         // "mgnl-resources" is re-included by LegacyClasspathResourceOrigin, we don't want any other mgnl-* folder for now except for mgnl-i18n.
204         filterBuilder.exclude("mgnl-bootstrap.*"); // exclude all folders starting with mgnl-bootstrap
205         filterBuilder.exclude("mgnl-resources.*"); // exclude mgnl-resources* (reflections uses package notation, not folders)
206         filterBuilder.exclude("mgnl-nodetypes.*"); // ditto for mgnl-i18n
207         filterBuilder.exclude("mgnl-files.*"); // ditto for mgnl-files
208         filterBuilder.include("mgnl-i18n.*"); // ditto for mgnl-i18n
209 
210         filterBuilder.exclude(extensionsPattern(excludedResourcesExtensions()));
211 
212         final String[] excludedPackages = excludedPackages();
213         for (String excludedPackage : excludedPackages) {
214             filterBuilder.excludePackage(excludedPackage);
215         }
216         return filterBuilder;
217     }
218 
219     private String[] excludedResourcesExtensions() {
220         return new String[]{
221                 "package\\.html", "java", "jar",
222                 // native libs (quite possibly covered by the com.* excludes but eh)
223                 "dylib", "dll", "jnilib", "class"
224         };
225     }
226 
227     private String[] excludedPackages() {
228         return new String[]{
229                 "META-INF",
230                 "com.oracle.java",
231                 "com.oracle.tools",
232                 "com.sun",
233                 "sun",
234                 "oracle", // Only found /oracle/jrockit/jfr/settings/jfc.xsd in my tests but eh
235                 "java",
236                 "javax",
237                 "jdk",
238                 "org.apache", // TODO is this too strict or arbitrary ?
239                 "lombok",
240                 "VAADIN",
241                 "gwt-unitCache"
242         };
243     }
244 
245     /**
246      * A predicate that matches a URL's toString() against a Matcher<String>.
247      */
248     private static class URLPredicate implements Predicate<URL> {
249 
250         public static Predicate<? super URL> excludedExtensions(String excludedUrlExtensionsPattern) {
251             final Predicate<CharSequence> exclude = Predicates.not(Predicates.containsPattern(excludedUrlExtensionsPattern));
252             return new URLPredicate(exclude);
253         }
254 
255         private final Predicate<CharSequence> predicate;
256 
257         protected URLPredicate(Predicate<CharSequence> predicate) {
258             this.predicate = predicate;
259         }
260 
261         @Override
262         public boolean apply(URL input) {
263             return predicate.apply(input.toString());
264         }
265     }
266 
267     private static class StartsWith implements Predicate<String> {
268 
269         private final List<String> prefixes;
270 
271         public StartsWith(List<String> prefixes) {
272             this.prefixes = prefixes;
273         }
274 
275         @Override
276         public boolean apply(String input) {
277             for (String prefix : prefixes) {
278                 if (input.startsWith(prefix)) {
279                     return true;
280                 }
281             }
282             return false;
283         }
284     }
285 }