Clover icon

Magnolia Resources Module 2.4.7

  1. Project Clover database Fri Sep 9 2016 16:21:46 CEST
  2. Package info.magnolia.module.resources.setup

File ResourceCleanUpTask.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart8.png
32% of files have more coverage

Code metrics

58
127
20
2
457
282
66
0.52
6.35
10
3.3

Classes

Class Line # Actions
ResourceCleanUpTask 105 127 0% 66 43
0.790243979%
ResourceCleanUpTask.CleanupMode 452 0 - 0 0
-1.0 -
 

Contributing tests

This file is covered by 12 tests. .

Source view

1    /**
2    * This file Copyright (c) 2016 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.module.resources.setup;
35   
36    import static info.magnolia.resourceloader.jcr.JcrResourceOrigin.*;
37   
38    import info.magnolia.cms.util.ClasspathResourcesUtil;
39    import info.magnolia.jcr.predicate.NodeTypePredicate;
40    import info.magnolia.jcr.util.NodeTypes;
41    import info.magnolia.jcr.util.NodeUtil;
42    import info.magnolia.jcr.util.PropertyUtil;
43    import info.magnolia.module.InstallContext;
44    import info.magnolia.module.delta.AbstractRepositoryTask;
45    import info.magnolia.module.delta.TaskExecutionException;
46    import info.magnolia.module.resources.ResourceTypes;
47    import info.magnolia.objectfactory.Components;
48    import info.magnolia.resourceloader.Resource;
49    import info.magnolia.resourceloader.ResourceOrigin;
50    import info.magnolia.resourceloader.jcr.JcrResourceOrigin;
51    import info.magnolia.resourceloader.layered.LayeredResource;
52    import info.magnolia.resourceloader.layered.LayeredResourceOrigin;
53   
54    import java.io.IOException;
55    import java.io.InputStream;
56    import java.io.Reader;
57    import java.util.ArrayList;
58    import java.util.Arrays;
59    import java.util.HashMap;
60    import java.util.Iterator;
61    import java.util.List;
62    import java.util.Map;
63    import java.util.Set;
64    import java.util.regex.Pattern;
65   
66    import javax.inject.Provider;
67    import javax.jcr.Node;
68    import javax.jcr.Property;
69    import javax.jcr.PropertyType;
70    import javax.jcr.RepositoryException;
71    import javax.jcr.Session;
72   
73    import org.apache.commons.collections4.CollectionUtils;
74    import org.apache.commons.io.FilenameUtils;
75    import org.apache.commons.io.IOUtils;
76    import org.apache.commons.lang3.ObjectUtils;
77    import org.apache.commons.lang3.StringUtils;
78    import org.apache.jackrabbit.JcrConstants;
79    import org.slf4j.Logger;
80    import org.slf4j.LoggerFactory;
81   
82    import com.google.common.base.Function;
83    import com.google.common.collect.ArrayListMultimap;
84    import com.google.common.collect.ListMultimap;
85    import com.google.common.collect.Lists;
86   
87    /**
88    * <b>Cleanup process for previously installed JCR resources.</b>
89    *
90    * <p>Since 5.4, a side effect of the new Resources API + app is that previously installed resources appear twice in the new app (one from the classpath with extension,
91    * one from JCR without extension). </p>
92    *
93    * <p>Installing resources was traditionally how resources used to be accessed, over <code>URI2ResourcesRepositoryMapping</code>, at <code>/resources</code>.
94    * This is still working and supported, but not encouraged in favor of the new <a href="https://documentation.magnolia-cms.com/display/DOCS/Resources" title="resource loading cascade">resource loading cascade</a>.
95    *
96    * <ul>
97    * <li>Resources had to be installed in order to be found by the URI2Repository mapping</li>
98    * <li>Those installed resources had to be bypassed in order to be served from the classpath instead.</li>
99    * </ul>
100    *
101    * Also, historically, dots were not allowed in JCR node names, hence why extension used to be stripped.</p>
102    *
103    * <p>{@linkplain ResourceCleanUpTask} tries to restore extension in JCR node names to resolve this problem.</p>
104    */
 
105    public class ResourceCleanUpTask extends AbstractRepositoryTask {
106   
107    private static final Logger log = LoggerFactory.getLogger(ResourceCleanUpTask.class);
108   
109    protected static final String PROPERTY_TEMPLATE = "mgnl:template";
110    protected static final String PROPERTY_EXTENSION = "extension";
111    protected static final String PROPERTY_TEXT = "text";
112    protected static final String PROPERTY_REFERENCE = "reference";
113    protected static final String RESOURCES_PREFIX = ResourceTypes.RESOURCES_PREFIX;
114    protected static final String BINARY_RESOURCES_TEMPLATE = ResourceTypes.BINARY;
115    protected static final String REFERENCES_RESOURCES_TEMPLATE = RESOURCES_PREFIX + "reference";
116    protected static final String RESOURCE_PROCESSED = RESOURCES_PREFIX + ResourceTypes.PROCESSED_PREFIX;
117   
118    private final Provider<ResourceOrigin> resourceOriginProvider;
119    private final CleanupMode cleanupMode;
120    private String parentPath;
121    private Pattern pattern;
122    private Set<String> absPaths;
123    private final UpdateResourceReferencesTask onSuccess;
124   
125    private final Map<String, String> changedPaths = new HashMap<>();
126    private final ListMultimap<UpdateResourceMessage, String[]> messages = ArrayListMultimap.create();
127   
128    private Session resourcesSession;
129   
 
130  14 toggle private ResourceCleanUpTask(String name, String description, CleanupMode cleanupMode, UpdateResourceReferencesTask onSuccess) {
131  14 super(name, description);
132  14 this.resourceOriginProvider = new Provider<ResourceOrigin>() {
 
133  12 toggle @Override
134    public ResourceOrigin get() {
135  12 return Components.getComponent(ResourceOrigin.class);
136    }
137    };
138  14 this.cleanupMode = cleanupMode;
139  14 this.onSuccess = onSuccess;
140    }
141   
142    /**
143    * Restores resource extension in JCR node names, by visiting a JCR sub-tree recursively, from the given parent path.
144    *
145    * <ul>
146    * <li>First the task resolves extension for resource nodes, by the <code>extension</code> property, binary node <code>extension</code> property, or resource template</li>
147    * <li>Then it tries to restore this extension if it's not in node-name already</li>
148    * </ul>
149    *
150    * <p>If resources already exist *in JCR* with extension and same content, they are considered redundant and are removed.<br/>
151    * If resources already exist on classpath or file-system with extension and same content, they are renamed to restore extension;
152    * mind that they are kept in JCR so that bypassed resources keep working via URI2RepositoryMapping.
153    * </p>
154    *
155    * @param parentPath a path in JCR resources workspace, to start visiting from
156    * @param onSuccess an {@link UpdateResourceReferencesTask} might be needed to update resource references for renamed resources
157    */
 
158  12 toggle public ResourceCleanUpTask(String name, String description, String parentPath, UpdateResourceReferencesTask onSuccess) {
159  12 this(name, description, CleanupMode.VISIT_JCR_TREE, onSuccess);
160  12 if (parentPath == null) {
161  0 throw new IllegalArgumentException("parentPath cannot be null");
162    }
163  12 this.parentPath = parentPath;
164    }
165   
166    /**
167    * Restores resource extension in JCR node names, for resources matching a given pattern.
168    *
169    * <ul>
170    * <li>First the task looks up for resource files matching the pattern in module resources</li>
171    * <li>Then it tries to find corresponding nodes in JCR, with extension stripped and same content</li>
172    * </ul>
173    *
174    * <p>If resources already exist *in JCR* with extension and same content, they are considered redundant and are removed.<br/>
175    * If resources already exist on classpath or file-system with extension and same content, they are renamed to restore extension;
176    * mind that they are kept in JCR so that bypassed resources keep working via URI2RepositoryMapping.
177    * </p>
178    *
179    * @param pathPattern a {@link Pattern} to match resource paths in module resources, including extension.
180    * @param onSuccess an {@link UpdateResourceReferencesTask} might be needed to update resource references for renamed resources
181    */
 
182  1 toggle public ResourceCleanUpTask(String name, String description, Pattern pathPattern, UpdateResourceReferencesTask onSuccess) {
183  1 this(name, description, CleanupMode.MATCH_CLASSPATH_RESOURCES_BY_PATTERN, onSuccess);
184  1 if (pathPattern == null) {
185  0 throw new IllegalArgumentException("pathPattern cannot be null");
186    }
187  1 this.pattern = pathPattern;
188    }
189   
190    /**
191    * Restores resource extension in JCR node names, for a set of specific resources.
192    *
193    * <ul>
194    * <li>First the task looks up for resource files in module resources</li>
195    * <li>Then it tries to find corresponding nodes in JCR, with extension stripped and same content</li>
196    * </ul>
197    *
198    * <p>If resources already exist *in JCR* with extension and same content, they are considered redundant and are removed.<br/>
199    * If resources already exist on classpath or file-system with extension and same content, they are renamed to restore extension;
200    * mind that they are kept in JCR so that bypassed resources keep working via URI2RepositoryMapping.
201    * </p>
202    *
203    * @param absPaths a set of existing resource paths in module resources, including extension.
204    * @param onSuccess an {@link UpdateResourceReferencesTask} might be needed to update resource references for renamed resources
205    */
 
206  1 toggle public ResourceCleanUpTask(String name, String description, Set<String> absPaths, UpdateResourceReferencesTask onSuccess) {
207  1 this(name, description, CleanupMode.MATCH_CLASSPATH_RESOURCES_BY_PATHS, onSuccess);
208  1 if (absPaths == null) {
209  0 throw new IllegalArgumentException("absPaths cannot be null");
210    }
211  1 this.absPaths = absPaths;
212    }
213   
 
214  12 toggle @Override
215    protected void doExecute(InstallContext installContext) throws RepositoryException, TaskExecutionException {
216   
217  12 resourcesSession = installContext.getJCRSession(RESOURCES_WORKSPACE);
218   
219  12 ResourceOrigin origin = resourceOriginProvider.get();
220   
221  12 if (!(origin instanceof LayeredResourceOrigin)) {
222  0 return;
223    }
224   
225  12 final LayeredResourceOrigin resourceOrigin = (LayeredResourceOrigin) origin;
226   
227  12 final Iterator<Node> resourcesIterator = findResourceNodes();
228   
229  23 while (resourcesIterator.hasNext()) {
230  11 Node resourceNode = resourcesIterator.next();
231  11 String extension = resolveResourceExtension(resourceNode);
232    // clean up properties after resolving extension (silly case of STK special resources, all installed as css)
233  11 cleanupProperties(resourceNode);
234   
235  11 if (StringUtils.isNotBlank(extension)) {
236    // For the case node name does not have extension
237    // When resource exists in resource origin check whether it was 'hot-fixed' or not
238  9 final String resourceNodePath = resourceNode.getPath();
239  9 final String resourcePath = resourceNodePath + "." + extension;
240  9 if (resourceOrigin.hasPath(resourcePath)) {
241  4 final LayeredResource layeredResource = resourceOrigin.getByPath(resourcePath);
242    // Check if they have same contents or not
243  4 if (equalContents(resourceNode, layeredResource.getFirst())) {
244  3 if (layeredResource.getFirst().getOrigin() instanceof JcrResourceOrigin) {
245  0 resourceNode.remove();
246  0 changedPaths.put(resourceNodePath, resourcePath);
247  0 messages.put(UpdateResourceMessage.EXISTS_JCR_AND_SAME_CONTENT, new String[] { resourceNodePath });
248    } else {
249  3 renameResource(resourceNode, extension);
250  3 messages.put(UpdateResourceMessage.NODE_RENAMED, new String[] { resourceNodePath, resourcePath });
251    }
252    } else {
253    // Resource with extension exists in origin, but contents are not the same
254  1 messages.put(UpdateResourceMessage.EXISTS_BUT_DIFFERENT_CONTENT, new String[] { resourceNodePath });
255    }
256  5 } else if (StringUtils.isEmpty(FilenameUtils.getExtension(resourceNode.getName()))) {
257    // When node name does not have dot (.)
258    // Rename node and keep track
259  1 renameResource(resourceNode, extension);
260  1 messages.put(UpdateResourceMessage.NODE_RENAMED, new String[] { resourceNodePath, resourcePath });
261    }
262    }
263    }
264   
265  12 UpdateResourceMessage.logMessages(installContext, log, messages);
266   
267    // Execute addition task
268  12 if (onSuccess != null && !changedPaths.isEmpty()) {
269  0 onSuccess.setChangedPaths(changedPaths);
270  0 onSuccess.execute(installContext);
271    }
272    }
273   
274    /**
275    * Cleans up empty magnolia properties (e.g. rights, source, version, modelClass);
276    * cleans up properties of binary nodes too (no modelClass and size of type Long)
277    */
 
278  11 toggle private void cleanupProperties(Node resourceNode) throws RepositoryException {
279  11 final List<String> magnoliaEmptyProperties = Lists.newArrayList("rights", "source", "version", "modelClass", PROPERTY_EXTENSION, PROPERTY_TEMPLATE);
280  11 for (String emptyProperty : magnoliaEmptyProperties) {
281  66 if (resourceNode.hasProperty(emptyProperty)) {
282  17 Property property = resourceNode.getProperty(emptyProperty);
283  17 if (property.getString().isEmpty()) {
284  6 property.remove();
285    }
286    }
287    }
288   
289  11 if (resourceNode.hasProperty(PROPERTY_TEMPLATE)
290    && resourceNode.getProperty(PROPERTY_TEMPLATE).getString().equals(BINARY_RESOURCES_TEMPLATE)
291    && resourceNode.hasNode(BINARY_NODE_NAME)) {
292  2 if (resourceNode.hasProperty("modelClass")) {
293    // modelClasses don't make any sense for binary resources
294  1 resourceNode.getProperty("modelClass").remove();
295    }
296  2 Node binary = resourceNode.getNode(BINARY_NODE_NAME);
297  2 if (binary.hasProperty("size")) {
298  1 Property size = binary.getProperty("size");
299  1 if (size.getType() == PropertyType.STRING) {
300  1 size.setValue(Long.parseLong(size.getString()));
301    }
302    }
303    }
304    }
305   
 
306  5 toggle protected Map<String, String> getChangedPaths() {
307  5 return changedPaths;
308    }
309   
 
310  12 toggle protected Iterator<Node> findResourceNodes() throws RepositoryException {
311  12 switch (cleanupMode) {
312  1 case MATCH_CLASSPATH_RESOURCES_BY_PATHS:
313  1 return findResourceNodesByPaths(absPaths).iterator();
314  1 case MATCH_CLASSPATH_RESOURCES_BY_PATTERN:
315  1 return findResourceNodesByPattern(pattern).iterator();
316  10 case VISIT_JCR_TREE:
317  10 Node parentNode = parentPath != null ? resourcesSession.getNode(parentPath) : resourcesSession.getRootNode();
318  10 return NodeUtil.collectAllChildren(parentNode, new NodeTypePredicate(NodeTypes.Content.NAME)).iterator();
319    }
320  0 return null;
321    }
322   
 
323  1 toggle private List<Node> findResourceNodesByPaths(final Set<String> paths) throws RepositoryException {
324  1 List<String> foundResourcePaths = Arrays.asList(ClasspathResourcesUtil.findResources(new ClasspathResourcesUtil.Filter() {
325   
 
326  1 toggle @Override
327    public boolean accept(String path) {
328  1 return paths.contains(path);
329    }
330    }));
331   
332    // Log message for resource not found in classpath.
333  1 if (!CollectionUtils.isEqualCollection(paths, foundResourcePaths)) {
334  0 List<String> givenPaths = Lists.newArrayList(paths);
335  0 givenPaths.removeAll(foundResourcePaths);
336  0 messages.putAll(UpdateResourceMessage.RESOURCE_NOT_FOUND_CLASSPATH, Lists.transform(givenPaths, new Function<String, String[]>() {
 
337  0 toggle @Override
338    public String[] apply(String input) {
339  0 return new String[] { input };
340    }
341    }));
342    }
343   
344  1 return getStrippedNodesByPaths(foundResourcePaths);
345    }
346   
 
347  1 toggle private List<Node> findResourceNodesByPattern(final Pattern pattern) throws RepositoryException {
348  1 List<String> foundResourcePaths = Arrays.asList(ClasspathResourcesUtil.findResources(pattern));
349  1 return getStrippedNodesByPaths(foundResourcePaths);
350    }
351   
 
352  2 toggle private List<Node> getStrippedNodesByPaths(final List<String> paths) throws RepositoryException {
353  2 List<Node> nodes = new ArrayList<>();
354  2 for (String path : paths) {
355  0 String strippedPath = StringUtils.substringBeforeLast(path, FilenameUtils.EXTENSION_SEPARATOR_STR);
356  0 if (resourcesSession.nodeExists(strippedPath)) {
357  0 Node node = resourcesSession.getNode(strippedPath);
358   
359    // only consider node if it has expected extension here
360  0 if (path.equals(strippedPath) || path.equals(strippedPath + "." + resolveResourceExtension(node))) {
361  0 nodes.add(node);
362  0 continue;
363    }
364    }
365  0 messages.put(UpdateResourceMessage.RESOURCE_NOT_FOUND_JCR, new String[] { path });
366    }
367  2 return nodes;
368    }
369   
 
370  11 toggle private String resolveResourceExtension(Node resourceNode) throws RepositoryException {
371  11 String template = PropertyUtil.getString(resourceNode, PROPERTY_TEMPLATE);
372   
373  11 if (REFERENCES_RESOURCES_TEMPLATE.equals(template)) {
374  0 return null;
375    }
376   
377  11 Node tempNode = (BINARY_RESOURCES_TEMPLATE.equals(template)) ? resourceNode.getNode(JcrResourceOrigin.BINARY_NODE_NAME) : resourceNode;
378   
379  11 if (tempNode.hasProperty(PROPERTY_EXTENSION)) {
380  10 return PropertyUtil.getString(tempNode, PROPERTY_EXTENSION);
381    }
382   
383  1 return StringUtils.lowerCase(StringUtils.substringAfter(template, (StringUtils.startsWith(template, RESOURCE_PROCESSED)) ? RESOURCE_PROCESSED : RESOURCES_PREFIX));
384    }
385   
386    /**
387    * Compare contents inside JCR resource node and other resource origin.
388    */
 
389  4 toggle private boolean equalContents(Node resourceNode, Resource resource) throws RepositoryException {
390  4 String template = PropertyUtil.getString(resourceNode, PROPERTY_TEMPLATE);
391  4 if (BINARY_RESOURCES_TEMPLATE.equals(template)) {
392  1 Node binaryNode = resourceNode.getNode(JcrResourceOrigin.BINARY_NODE_NAME);
393   
394  1 return equalBinaryContents(binaryNode, resource);
395    } else {
396  3 return equalTextContents(resourceNode, resource);
397    }
398    }
399   
 
400  1 toggle private boolean equalBinaryContents(Node binaryNode, Resource binaryResource) throws RepositoryException {
401  1 try (final InputStream nodeStream = binaryNode.getProperty(JcrConstants.JCR_DATA).getBinary().getStream();
402  1 final InputStream resourceStream = binaryResource.openStream()) {
403   
404  1 return IOUtils.contentEquals(nodeStream, resourceStream);
405    } catch (IOException e) {
406  0 log.error("Cannot read contents of '{}:{}'.", binaryResource.getOrigin().getName(), binaryResource.getName(), e);
407    }
408   
409  0 return false;
410    }
411   
 
412  3 toggle private boolean equalTextContents(Node resourceNode, Resource resource) {
413  3 try (final Reader reader = resource.openReader()) {
414  3 String contentResourceNode = PropertyUtil.getString(resourceNode, PROPERTY_TEXT);
415  3 String contentResource = IOUtils.toString(reader);
416   
417  3 return ObjectUtils.equals(contentResourceNode, contentResource);
418    } catch (IOException e) {
419  0 log.error("Cannot read contents of '{}:{}'.", resource.getOrigin().getName(), resource.getName(), e);
420    }
421   
422  0 return false;
423    }
424   
425    /**
426    * Restores file extension in node name and removes redundant extension property.
427    * If this is a binary resource, also restores file extension on the binary node's fileName property.
428    */
 
429  4 toggle private void renameResource(Node resourceNode, String extension) throws RepositoryException {
430  4 String oldPath = resourceNode.getPath();
431  4 String newPath = resourceNode.getPath() + "." + extension;
432  4 resourcesSession.move(oldPath, newPath);
433  4 removeExtensionProperty(resourceNode);
434    // clean up binary node fileName property
435  4 if (resourceNode.hasProperty(PROPERTY_TEMPLATE)
436    && resourceNode.getProperty(PROPERTY_TEMPLATE).getString().equals(BINARY_RESOURCES_TEMPLATE)
437    && resourceNode.hasNode(BINARY_NODE_NAME)) {
438  1 Node binary = resourceNode.getNode(BINARY_NODE_NAME);
439  1 if (binary.hasProperty("fileName")) {
440  1 binary.getProperty("fileName").setValue(StringUtils.substringAfterLast(newPath, "/"));
441    }
442    }
443  4 changedPaths.put(oldPath, newPath);
444    }
445   
 
446  4 toggle private void removeExtensionProperty(Node resourceNode) throws RepositoryException {
447  4 if (resourceNode.hasProperty(PROPERTY_EXTENSION)) {
448  3 resourceNode.getProperty(PROPERTY_EXTENSION).remove();
449    }
450    }
451   
 
452    private enum CleanupMode {
453    VISIT_JCR_TREE,
454    MATCH_CLASSPATH_RESOURCES_BY_PATHS,
455    MATCH_CLASSPATH_RESOURCES_BY_PATTERN
456    }
457    }