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