View Javadoc
1   /**
2    * This file Copyright (c) 2015-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.module.resources.setup;
35  
36  import info.magnolia.jcr.util.NodeTypes;
37  import info.magnolia.jcr.util.NodeUtil;
38  import info.magnolia.jcr.util.PropertyUtil;
39  import info.magnolia.module.InstallContext;
40  import info.magnolia.module.delta.AbstractRepositoryTask;
41  import info.magnolia.module.delta.Task;
42  import info.magnolia.module.delta.TaskExecutionException;
43  import info.magnolia.objectfactory.Components;
44  import info.magnolia.repository.RepositoryManager;
45  import info.magnolia.resourceloader.Resource;
46  import info.magnolia.resourceloader.ResourceOrigin;
47  import info.magnolia.resourceloader.jcr.JcrResourceOrigin;
48  import info.magnolia.resourceloader.layered.LayeredResource;
49  import info.magnolia.resourceloader.layered.LayeredResourceOrigin;
50  
51  import java.io.IOException;
52  import java.io.Reader;
53  
54  import javax.inject.Provider;
55  import javax.jcr.Node;
56  import javax.jcr.Property;
57  import javax.jcr.PropertyIterator;
58  import javax.jcr.RepositoryException;
59  import javax.jcr.Session;
60  
61  import org.apache.commons.io.IOUtils;
62  import org.apache.commons.lang3.ObjectUtils;
63  import org.apache.commons.lang3.StringUtils;
64  import org.apache.jackrabbit.commons.predicate.NodeTypePredicate;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * Migrates all templates from <code>{@value #TEMPLATES_WORKSPACE}</code> to the
70   * <code>{@value JcrResourceOrigin#RESOURCES_WORKSPACE}</code> workspace.
71   *
72   * <p>Templates will only be processed when they have the <code>{@value #PROPERTY_ENABLED}</code> property.</p>
73   *
74   * <p>When a template has <code>{@value #PROPERTY_AUTO_IMPORT}</code> set to <code>true</code> we check if there is an
75   * existing resource in the {@link ResourceOrigin} and migrate the template if there is none. Otherwise no action is
76   * taken.</p>
77   *
78   * <p>When a template has <code>{@value #PROPERTY_AUTO_IMPORT}</code> set to <code>false</code> we check whether there
79   * is a hot-fixed version of that template in {@link JcrResourceOrigin#RESOURCES_WORKSPACE}. If there is, contents
80   * are compared and when equal, no action is taken, otherwise this task will fail! If there is no hot-fix or no
81   * resource what so ever, the template will be migrated.</p>
82   */
83  public class TemplateMigrationTask extends AbstractRepositoryTask {
84  
85      private static final Logger log = LoggerFactory.getLogger(TemplateMigrationTask.class);
86  
87      protected static final String TEMPLATES_WORKSPACE = "templates";
88  
89      protected static final String PROPERTY_AUTO_IMPORT = "autoImport";
90      protected static final String PROPERTY_TEXT = "text";
91      protected static final String PROPERTY_ENABLED = "enabled";
92  
93      private final Provider<ResourceOrigin> resourceOriginProvider;
94  
95      private Session templatesSession = null;
96      private Session resourcesSession = null;
97  
98      private final Task onSuccess;
99  
100     public TemplateMigrationTask(Task onSuccess) {
101         super("Migrates templates from 'templates' workspace", "Migrates templates from 'templates' workspace to 'resources' workspace");
102 
103         this.resourceOriginProvider = () -> Components.getComponent(ResourceOrigin.class);
104 
105         this.onSuccess = onSuccess;
106     }
107 
108     public TemplateMigrationTask() {
109         this(null);
110     }
111 
112     @Override
113     protected void doExecute(InstallContext installContext) throws RepositoryException, TaskExecutionException {
114         final RepositoryManager repositoryManager = Components.getComponent(RepositoryManager.class);
115         String infoMessage = null;
116         if (repositoryManager.hasWorkspace(TEMPLATES_WORKSPACE)) {
117             try {
118                 templatesSession = installContext.getJCRSession(TEMPLATES_WORKSPACE);
119             } catch (RepositoryException e) {
120                 infoMessage = String.format("Could not get session for workspace '%s'. " +
121                         "Not running template migration task.", TEMPLATES_WORKSPACE);
122             }
123         } else {
124             infoMessage = "Inplace-templating might not be installed. Not running template migration task.";
125         }
126 
127         if (infoMessage != null) {
128             installContext.info(infoMessage);
129             log.info(infoMessage);
130 
131             // We won't exit here as there might be an onSuccessTask to be executed below. Not having to do anything
132             // with the templates workspace doesn't mean we are done yet.
133         }
134 
135         if (templatesSession != null) {
136             resourcesSession = installContext.getJCRSession(JcrResourceOrigin.RESOURCES_WORKSPACE);
137 
138             final LayeredResourceOrigin resourceOrigin = (LayeredResourceOrigin) resourceOriginProvider.get();
139 
140             for (Node templateNode : NodeUtil.collectAllChildren(templatesSession.getRootNode(),
141                     new NodeTypePredicate(NodeTypes.Content.NAME, false))) {
142                 // We consider no enabled property to be equal to enabled=false
143                 // All templates used to be considered as disabled, also see deprecated
144                 // info.magnolia.module.inplacetemplating.JcrRepoTemplateLoader
145                 final boolean enabled = templateNode.hasProperty(PROPERTY_ENABLED) && templateNode.getProperty(PROPERTY_ENABLED).getBoolean();
146                 if (!enabled) {
147                     continue;
148                 }
149 
150                 // We consider no autoImport property to be equal to autoImport=false
151                 final boolean autoImport = templateNode.hasProperty(PROPERTY_AUTO_IMPORT) && templateNode.getProperty(PROPERTY_AUTO_IMPORT).getBoolean();
152 
153                 try {
154 
155                     final String resourceTemplatePath = templateNode.getPath() + ".ftl";
156 
157                     // autoImport=true means we can safely override edited resources
158                     // autoImport=false means we cannot
159                     if (autoImport) {
160                         if (!resourceOrigin.hasPath(resourceTemplatePath)) {
161                             // Template doesn't exists in resource origin so we can migrate it
162                             migrateTemplate(templateNode, resourceTemplatePath);
163                         }
164                     } else {
165                         // When template exists in resource origin check whether it was 'hot-fixed' or not
166                         if (resourceOrigin.hasPath(resourceTemplatePath)) {
167                             final LayeredResource resource = resourceOrigin.getByPath(resourceTemplatePath);
168 
169                             // When there is a hot-fixed resource
170                             if (resource.getFirst().getOrigin() instanceof JcrResourceOrigin) {
171                                 // Compare contents
172                                 if (!templateEqualsResource(templateNode, resource)) {
173                                     // There is no way we can handle this scenario:
174                                     // autoImport=false (i.e. do not override) but the very same template exists in jcr
175                                     // resource origin with different content!
176                                     throw new TaskExecutionException(String.format("Contents of '%s:%s' and resource '%s:%s' were not equal but autoImport was set to 'false'. " +
177                                                     "Cannot proceed with template migration.", TEMPLATES_WORKSPACE, resourceTemplatePath,
178                                             JcrResourceOrigin.RESOURCES_WORKSPACE, resource.getPath()));
179                                 }
180                                 // Otherwise do nothing with template; it's already in jcr with the same content
181                                 // why bother migrating it...!?
182                             } else {
183                                 // No jcr hotfix available means we can safely migrate
184                                 migrateTemplate(templateNode, resourceTemplatePath);
185                             }
186                         } else {
187                             // No resource available at all also means we can safely migrate the template to jcr
188                             migrateTemplate(templateNode, resourceTemplatePath);
189                         }
190                     }
191 
192                 } catch (RepositoryException e) {
193                     log.info("An error occurred when handling template '{}:{}'. Will not migrate this template.",
194                             TEMPLATES_WORKSPACE, NodeUtil.getPathIfPossible(templateNode), e);
195                 }
196             }
197 
198         }
199 
200         // If we reach this point, no errors have happened!
201         // Any additional task given, will now be executed...
202         if (onSuccess != null) {
203             onSuccess.execute(installContext);
204         }
205     }
206 
207     /**
208      * Compares the template contents of a {@link Node} from <code>{@value #TEMPLATES_WORKSPACE}</code> workspace and a
209      * {@link Resource} from {@link ResourceOrigin}. Both contents being {@code null} are considered equal.
210      */
211     private boolean templateEqualsResource(final Node templateNode, final Resource resource) {
212         try (final Reader reader = resource.openReader()) {
213             final String contentResource = IOUtils.toString(reader);
214             final String contentTemplate = PropertyUtil.getString(templateNode, PROPERTY_TEXT, null);
215 
216             return ObjectUtils.equals(contentResource, contentTemplate);
217         } catch (IOException e) {
218             log.error("Cannot read contents of '{}:{}'.", resource.getOrigin().getName(), resource.getName(), e);
219         }
220 
221         return false;
222     }
223 
224     /**
225      * Copies the template {@link Node} to the {@value JcrResourceOrigin#RESOURCES_WORKSPACE} workspace. Also creates
226      * parent folders if necessary.
227      */
228     private void migrateTemplate(final Node templateNode, final String newTemplatePath) throws RepositoryException {
229         final String parentPath = StringUtils.substringBeforeLast(newTemplatePath, "/");
230         if (StringUtils.isNotBlank(parentPath) && !resourcesSession.nodeExists(parentPath)) {
231             NodeUtil.createPath(resourcesSession.getRootNode(), parentPath, NodeTypes.Folder.NAME);
232 
233             log.info("Created path '{}:{}'.", JcrResourceOrigin.RESOURCES_WORKSPACE, parentPath);
234         }
235 
236         copyTemplateNode(templateNode, newTemplatePath);
237 
238         log.info("Migrated template '{}:{}' to '{}:{}'.", TEMPLATES_WORKSPACE, NodeUtil.getPathIfPossible(templateNode),
239                 JcrResourceOrigin.RESOURCES_WORKSPACE, newTemplatePath);
240     }
241 
242     /**
243      * Copies {@link Node} to destination <code>newTemplatePath</code> and also copies all properties (except
244      * properties suffixed with <code>{@value NodeTypes#REP_PREFIX}</code> or
245      * <code>{@value NodeTypes#JCR_PREFIX}</code>) without saving the session.
246      *
247      * <p>Note: {@link javax.jcr.Workspace#clone(String, String, String, boolean)} or
248      * {@link javax.jcr.Workspace#copy(String, String, String)} would have needed the parent folders to be persisted
249      * ({@link Session#save()}) which is not desired in {@link info.magnolia.module.delta.Task}s!</p>
250      */
251     private void copyTemplateNode(final Node templateNode, final String newTemplatePath) throws RepositoryException {
252         final Node newTemplateNode = NodeUtil.createPath(resourcesSession.getRootNode(), newTemplatePath, NodeTypes.Content.NAME);
253         final PropertyIterator propertyIterator = templateNode.getProperties();
254 
255         while (propertyIterator.hasNext()) {
256             final Property property = propertyIterator.nextProperty();
257             final String propertyName = property.getName();
258 
259             // We do not migrate property enabled
260             if (PROPERTY_ENABLED.equals(propertyName)) {
261                 continue;
262             }
263 
264             // Properties prefixed with jcr:/rep: should not be migrated
265             if (propertyName.startsWith(NodeTypes.REP_PREFIX) || propertyName.startsWith(NodeTypes.JCR_PREFIX)) {
266                 continue;
267             }
268 
269             // We want to maintain the last modified date/actor of above, new node
270             // Creation date/actor will be migrated
271             if (NodeTypes.LastModified.LAST_MODIFIED.equals(propertyName) || NodeTypes.LastModified.LAST_MODIFIED_BY.equals(propertyName)) {
272                 continue;
273             }
274 
275             // We also do not want to copy properties related to activation. This makes sure the new nodes do not have
276             // the activation status + information until they are indeed properly activated.
277             // This is also "in-sync" with potentially created parent folders that have the not-activated status.
278             if (NodeTypes.Activatable.LAST_ACTIVATED.equals(propertyName) || NodeTypes.Activatable.LAST_ACTIVATED_BY.equals(propertyName) ||
279                     NodeTypes.Activatable.ACTIVATION_STATUS.equals(propertyName)) {
280                 continue;
281             }
282 
283             newTemplateNode.setProperty(propertyName, property.getValue());
284         }
285     }
286 
287 }