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