1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 import java.util.stream.Collectors;
66
67 import javax.inject.Provider;
68 import javax.jcr.Node;
69 import javax.jcr.Property;
70 import javax.jcr.PropertyType;
71 import javax.jcr.RepositoryException;
72 import javax.jcr.Session;
73
74 import org.apache.commons.collections4.CollectionUtils;
75 import org.apache.commons.io.FilenameUtils;
76 import org.apache.commons.io.IOUtils;
77 import org.apache.commons.lang3.ObjectUtils;
78 import org.apache.commons.lang3.StringUtils;
79 import org.apache.jackrabbit.JcrConstants;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 import com.google.common.collect.ArrayListMultimap;
84 import com.google.common.collect.ListMultimap;
85 import com.google.common.collect.Lists;
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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 private ResourceCleanUpTask(String name, String description, CleanupMode cleanupMode, UpdateResourceReferencesTask onSuccess) {
131 super(name, description);
132 this.resourceOriginProvider = () -> Components.getComponent(ResourceOrigin.class);
133 this.cleanupMode = cleanupMode;
134 this.onSuccess = onSuccess;
135 }
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153 public ResourceCleanUpTask(String name, String description, String parentPath, UpdateResourceReferencesTask onSuccess) {
154 this(name, description, CleanupMode.VISIT_JCR_TREE, onSuccess);
155 if (parentPath == null) {
156 throw new IllegalArgumentException("parentPath cannot be null");
157 }
158 this.parentPath = parentPath;
159 }
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177 public ResourceCleanUpTask(String name, String description, Pattern pathPattern, UpdateResourceReferencesTask onSuccess) {
178 this(name, description, CleanupMode.MATCH_CLASSPATH_RESOURCES_BY_PATTERN, onSuccess);
179 if (pathPattern == null) {
180 throw new IllegalArgumentException("pathPattern cannot be null");
181 }
182 this.pattern = pathPattern;
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201 public ResourceCleanUpTask(String name, String description, Set<String> absPaths, UpdateResourceReferencesTask onSuccess) {
202 this(name, description, CleanupMode.MATCH_CLASSPATH_RESOURCES_BY_PATHS, onSuccess);
203 if (absPaths == null) {
204 throw new IllegalArgumentException("absPaths cannot be null");
205 }
206 this.absPaths = absPaths;
207 }
208
209 @Override
210 protected void doExecute(InstallContext installContext) throws RepositoryException, TaskExecutionException {
211
212 resourcesSession = installContext.getJCRSession(RESOURCES_WORKSPACE);
213
214 ResourceOrigin origin = resourceOriginProvider.get();
215
216 if (!(origin instanceof LayeredResourceOrigin)) {
217 return;
218 }
219
220 final LayeredResourceOrigin resourceOrigin = (LayeredResourceOrigin) origin;
221
222 final Iterator<Node> resourcesIterator = findResourceNodes();
223
224 while (resourcesIterator.hasNext()) {
225 Node resourceNode = resourcesIterator.next();
226 String extension = resolveResourceExtension(resourceNode);
227
228 cleanupProperties(resourceNode);
229
230 if (StringUtils.isNotBlank(extension)) {
231
232
233 final String resourceNodePath = resourceNode.getPath();
234 final String resourcePath = resourceNodePath + "." + extension;
235 if (resourceOrigin.hasPath(resourcePath)) {
236 final LayeredResource layeredResource = resourceOrigin.getByPath(resourcePath);
237
238 if (equalContents(resourceNode, layeredResource.getFirst())) {
239 if (layeredResource.getFirst().getOrigin() instanceof JcrResourceOrigin) {
240 resourceNode.remove();
241 changedPaths.put(resourceNodePath, resourcePath);
242 messages.put(UpdateResourceMessage.EXISTS_JCR_AND_SAME_CONTENT, new String[] { resourceNodePath });
243 } else {
244 renameResource(resourceNode, extension);
245 messages.put(UpdateResourceMessage.NODE_RENAMED, new String[] { resourceNodePath, resourcePath });
246 }
247 } else {
248
249 messages.put(UpdateResourceMessage.EXISTS_BUT_DIFFERENT_CONTENT, new String[] { resourceNodePath });
250 }
251 } else if (StringUtils.isEmpty(FilenameUtils.getExtension(resourceNode.getName()))) {
252
253
254 renameResource(resourceNode, extension);
255 messages.put(UpdateResourceMessage.NODE_RENAMED, new String[] { resourceNodePath, resourcePath });
256 }
257 }
258 }
259
260 UpdateResourceMessage.logMessages(installContext, log, messages);
261
262
263 if (onSuccess != null && !changedPaths.isEmpty()) {
264 onSuccess.setChangedPaths(changedPaths);
265 onSuccess.execute(installContext);
266 }
267 }
268
269
270
271
272
273 private void cleanupProperties(Node resourceNode) throws RepositoryException {
274 final List<String> magnoliaEmptyProperties = Lists.newArrayList("rights", "source", "version", "modelClass", PROPERTY_EXTENSION, PROPERTY_TEMPLATE);
275 for (String emptyProperty : magnoliaEmptyProperties) {
276 if (resourceNode.hasProperty(emptyProperty)) {
277 Property property = resourceNode.getProperty(emptyProperty);
278 if (property.getString().isEmpty()) {
279 property.remove();
280 }
281 }
282 }
283
284 if (resourceNode.hasProperty(PROPERTY_TEMPLATE)
285 && resourceNode.getProperty(PROPERTY_TEMPLATE).getString().equals(BINARY_RESOURCES_TEMPLATE)
286 && resourceNode.hasNode(BINARY_NODE_NAME)) {
287 if (resourceNode.hasProperty("modelClass")) {
288
289 resourceNode.getProperty("modelClass").remove();
290 }
291 Node binary = resourceNode.getNode(BINARY_NODE_NAME);
292 if (binary.hasProperty("size")) {
293 Property size = binary.getProperty("size");
294 if (size.getType() == PropertyType.STRING) {
295 size.setValue(Long.parseLong(size.getString()));
296 }
297 }
298 }
299 }
300
301 protected Map<String, String> getChangedPaths() {
302 return changedPaths;
303 }
304
305 protected Iterator<Node> findResourceNodes() throws RepositoryException {
306 switch (cleanupMode) {
307 case MATCH_CLASSPATH_RESOURCES_BY_PATHS:
308 return findResourceNodesByPaths(absPaths).iterator();
309 case MATCH_CLASSPATH_RESOURCES_BY_PATTERN:
310 return findResourceNodesByPattern(pattern).iterator();
311 case VISIT_JCR_TREE:
312 Node parentNode = parentPath != null ? resourcesSession.getNode(parentPath) : resourcesSession.getRootNode();
313 return NodeUtil.collectAllChildren(parentNode, new NodeTypePredicate(NodeTypes.Content.NAME)).iterator();
314 }
315 return null;
316 }
317
318 private List<Node> findResourceNodesByPaths(final Set<String> paths) throws RepositoryException {
319 List<String> foundResourcePaths = Arrays.asList(ClasspathResourcesUtil.findResources(paths::contains));
320
321
322 if (!CollectionUtils.isEqualCollection(paths, foundResourcePaths)) {
323 List<String> givenPaths = Lists.newArrayList(paths);
324 givenPaths.removeAll(foundResourcePaths);
325 messages.putAll(UpdateResourceMessage.RESOURCE_NOT_FOUND_CLASSPATH, givenPaths.stream().map(input -> new String[]{input}).collect(Collectors.toList()));
326 }
327
328 return getStrippedNodesByPaths(foundResourcePaths);
329 }
330
331 private List<Node> findResourceNodesByPattern(final Pattern pattern) throws RepositoryException {
332 List<String> foundResourcePaths = Arrays.asList(ClasspathResourcesUtil.findResources(pattern));
333 return getStrippedNodesByPaths(foundResourcePaths);
334 }
335
336 private List<Node> getStrippedNodesByPaths(final List<String> paths) throws RepositoryException {
337 List<Node> nodes = new ArrayList<>();
338 for (String path : paths) {
339 String strippedPath = StringUtils.substringBeforeLast(path, FilenameUtils.EXTENSION_SEPARATOR_STR);
340 if (resourcesSession.nodeExists(strippedPath)) {
341 Node node = resourcesSession.getNode(strippedPath);
342
343
344 if (path.equals(strippedPath) || path.equals(strippedPath + "." + resolveResourceExtension(node))) {
345 nodes.add(node);
346 continue;
347 }
348 }
349 messages.put(UpdateResourceMessage.RESOURCE_NOT_FOUND_JCR, new String[] { path });
350 }
351 return nodes;
352 }
353
354 private String resolveResourceExtension(Node resourceNode) throws RepositoryException {
355 String template = PropertyUtil.getString(resourceNode, PROPERTY_TEMPLATE);
356
357 if (REFERENCES_RESOURCES_TEMPLATE.equals(template)) {
358 return null;
359 }
360
361 Node tempNode = (BINARY_RESOURCES_TEMPLATE.equals(template)) ? resourceNode.getNode(JcrResourceOrigin.BINARY_NODE_NAME) : resourceNode;
362
363 if (tempNode.hasProperty(PROPERTY_EXTENSION)) {
364 return PropertyUtil.getString(tempNode, PROPERTY_EXTENSION);
365 }
366
367 return StringUtils.lowerCase(StringUtils.substringAfter(template, (StringUtils.startsWith(template, RESOURCE_PROCESSED)) ? RESOURCE_PROCESSED : RESOURCES_PREFIX));
368 }
369
370
371
372
373 private boolean equalContents(Node resourceNode, Resource resource) throws RepositoryException {
374 String template = PropertyUtil.getString(resourceNode, PROPERTY_TEMPLATE);
375 if (BINARY_RESOURCES_TEMPLATE.equals(template)) {
376 Node binaryNode = resourceNode.getNode(JcrResourceOrigin.BINARY_NODE_NAME);
377
378 return equalBinaryContents(binaryNode, resource);
379 } else {
380 return equalTextContents(resourceNode, resource);
381 }
382 }
383
384 private boolean equalBinaryContents(Node binaryNode, Resource binaryResource) throws RepositoryException {
385 try (final InputStream nodeStream = binaryNode.getProperty(JcrConstants.JCR_DATA).getBinary().getStream();
386 final InputStream resourceStream = binaryResource.openStream()) {
387
388 return IOUtils.contentEquals(nodeStream, resourceStream);
389 } catch (IOException e) {
390 log.error("Cannot read contents of '{}:{}'.", binaryResource.getOrigin().getName(), binaryResource.getName(), e);
391 }
392
393 return false;
394 }
395
396 private boolean equalTextContents(Node resourceNode, Resource resource) {
397 try (final Reader reader = resource.openReader()) {
398 String contentResourceNode = PropertyUtil.getString(resourceNode, PROPERTY_TEXT);
399 String contentResource = IOUtils.toString(reader);
400
401 return ObjectUtils.equals(contentResourceNode, contentResource);
402 } catch (IOException e) {
403 log.error("Cannot read contents of '{}:{}'.", resource.getOrigin().getName(), resource.getName(), e);
404 }
405
406 return false;
407 }
408
409
410
411
412
413 private void renameResource(Node resourceNode, String extension) throws RepositoryException {
414 String oldPath = resourceNode.getPath();
415 String newPath = resourceNode.getPath() + "." + extension;
416 resourcesSession.move(oldPath, newPath);
417 removeExtensionProperty(resourceNode);
418
419 if (resourceNode.hasProperty(PROPERTY_TEMPLATE)
420 && resourceNode.getProperty(PROPERTY_TEMPLATE).getString().equals(BINARY_RESOURCES_TEMPLATE)
421 && resourceNode.hasNode(BINARY_NODE_NAME)) {
422 Node binary = resourceNode.getNode(BINARY_NODE_NAME);
423 if (binary.hasProperty("fileName")) {
424 binary.getProperty("fileName").setValue(StringUtils.substringAfterLast(newPath, "/"));
425 }
426 }
427 changedPaths.put(oldPath, newPath);
428 }
429
430 private void removeExtensionProperty(Node resourceNode) throws RepositoryException {
431 if (resourceNode.hasProperty(PROPERTY_EXTENSION)) {
432 resourceNode.getProperty(PROPERTY_EXTENSION).remove();
433 }
434 }
435
436 private enum CleanupMode {
437 VISIT_JCR_TREE,
438 MATCH_CLASSPATH_RESOURCES_BY_PATHS,
439 MATCH_CLASSPATH_RESOURCES_BY_PATTERN
440 }
441 }