View Javadoc
1   /**
2    * This file Copyright (c) 2012-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.ui.dialog.setup;
35  
36  import info.magnolia.cms.util.QueryUtil;
37  import info.magnolia.jcr.predicate.AbstractPredicate;
38  import info.magnolia.jcr.predicate.NodeTypePredicate;
39  import info.magnolia.jcr.util.NodeTypes;
40  import info.magnolia.jcr.util.NodeUtil;
41  import info.magnolia.jcr.util.NodeVisitor;
42  import info.magnolia.jcr.util.PropertyUtil;
43  import info.magnolia.module.InstallContext;
44  import info.magnolia.module.delta.AbstractTask;
45  import info.magnolia.module.delta.TaskExecutionException;
46  import info.magnolia.objectfactory.Components;
47  import info.magnolia.repository.RepositoryConstants;
48  import info.magnolia.ui.dialog.setup.migration.ActionCreator;
49  import info.magnolia.ui.dialog.setup.migration.BaseActionCreator;
50  import info.magnolia.ui.dialog.setup.migration.ControlMigrator;
51  import info.magnolia.ui.dialog.setup.migration.ControlMigratorsRegistry;
52  import info.magnolia.ui.form.field.definition.StaticFieldDefinition;
53  
54  import java.util.Arrays;
55  import java.util.HashMap;
56  import java.util.HashSet;
57  import java.util.Iterator;
58  import java.util.List;
59  
60  import javax.jcr.Node;
61  import javax.jcr.NodeIterator;
62  import javax.jcr.Property;
63  import javax.jcr.RepositoryException;
64  import javax.jcr.Session;
65  import javax.jcr.query.Query;
66  
67  import org.apache.commons.lang3.StringUtils;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  /**
72   * Dialog migration main task.<br>
73   * Migrate all dialogs defined within the specified module.<br>
74   */
75  public class DialogMigrationTask extends AbstractTask {
76  
77      private static final Logger log = LoggerFactory.getLogger(DialogMigrationTask.class);
78      private static final String PROPERTY_NAME_EXTENDS = "extends";
79      private static final String PROPERTY_NAME_REFERENCE = "reference";
80  
81      private final String moduleName;
82      private final HashSet<Property> extendsAndReferenceProperty = new HashSet<Property>();
83      private ControlMigratorsRegistry controlMigratorsRegistry;
84  
85      private HashMap<String, ControlMigrator> controlsToMigrate;
86      private String defaultDialogActions = "defaultDialogActions";
87      private HashMap<String, List<ActionCreator>> dialogActionsToMigrate;
88      private InstallContext installContext;
89  
90      private HashMap<String, ControlMigrator> customControlsToMigrate;
91      private HashMap<String, List<ActionCreator>> customDialogActionsToMigrate;
92  
93      /**
94       * @param taskName name of the task
95       * @param taskDescription short decription of the task
96       * @param moduleName all dialog define under this module name will be migrated.
97       * @param customControlsToMigrate Custom controls to migrate.
98       * @param customDialogActionsToMigrate Custom actions to migrate
99       */
100     public DialogMigrationTask(String taskName, String taskDescription, String moduleName, HashMap<String, ControlMigrator> customControlsToMigrate, HashMap<String, List<ActionCreator>> customDialogActionsToMigrate) {
101         super(taskName, taskDescription);
102         this.moduleName = moduleName;
103 
104         // Use Components else we will need to inject ControlMigratorsRegistry in all version handler that uses DialogMigrationTask.
105         // Version handler needs ControlMigratorsRegistry only for registrating custom dialogMigrators.
106         this.controlMigratorsRegistry = Components.getComponent(ControlMigratorsRegistry.class);
107         this.customControlsToMigrate = customControlsToMigrate;
108         this.customDialogActionsToMigrate = customDialogActionsToMigrate;
109     }
110 
111     public DialogMigrationTask(String taskName, String taskDescription, String moduleName) {
112         this(taskName, taskDescription, moduleName, null, null);
113     }
114 
115     public DialogMigrationTask(String moduleName) {
116         this("Dialog Migration for 5.x", "Migrate dialog for the following module: " + moduleName, moduleName, null, null);
117     }
118 
119     /**
120      * Handle all Dialogs registered and migrate them.
121      */
122     @Override
123     public void execute(InstallContext installContext) throws TaskExecutionException {
124         Session session;
125         this.installContext = installContext;
126         try {
127             registerControlsAndActionsMigrators();
128 
129             String dialogNodeName = "dialogs";
130             String dialogPath = "/modules/" + moduleName + "/" + dialogNodeName;
131             session = installContext.getJCRSession(RepositoryConstants.CONFIG);
132 
133             // Check
134             if (!session.itemExists(dialogPath)) {
135                 log.warn("Dialog definition do not exist for the following module {}. No Dialog migration task will be performed", moduleName);
136                 return;
137             }
138             Node dialog = session.getNode(dialogPath);
139             // Convert extends/reference path from relative to absolute.
140             resolveRelativeExtendsPath(dialog);
141             NodeUtil.visit(dialog, new NodeVisitor() {
142                 @Override
143                 public void visit(Node current) throws RepositoryException {
144                     for (Node dialogNode : NodeUtil.getNodes(current, NodeTypes.ContentNode.NAME)) {
145                         performDialogMigration(dialogNode);
146                     }
147                 }
148             }, new NodeTypePredicate(NodeTypes.Content.NAME));
149 
150             // Try to resolve references for extends.
151             postProcessForExtendsAndReference();
152 
153         } catch (Exception e) {
154             log.error("", e);
155             installContext.warn("Could not Migrate Dialog for the following module " + moduleName);
156             throw new TaskExecutionException("Could not Migrate Dialog ", e);
157         }
158     }
159 
160     private void registerControlsAndActionsMigrators() {
161         // Register default and passed during class initialization
162         registerControlsToMigrate(customControlsToMigrate);
163         registerDialogActionToCreate(customDialogActionsToMigrate);
164         // Add custom
165         addCustomControlsToMigrate(controlsToMigrate);
166         addCustomDialogActionToCreate(dialogActionsToMigrate);
167     }
168 
169     /**
170      * Register default UI controls to migrate.
171      */
172     private void registerControlsToMigrate(HashMap<String, ControlMigrator> customControlsToMigrate) {
173         this.controlsToMigrate = new HashMap<String, ControlMigrator>();
174         // Register default controls (defined in the ui-framwork version handler class)
175         this.controlsToMigrate.putAll(controlMigratorsRegistry.getAllMigrators());
176         // Register custom
177         if (customControlsToMigrate != null) {
178             this.controlsToMigrate.putAll(customControlsToMigrate);
179         }
180     }
181 
182     /**
183      * Override this method in order to register custom controls to migrate.<br>
184      * In case a control name is already define in the default map, the old control migrator is replaced by the newly registered control migrator.
185      *
186      * @param controlsToMigrate
187      * key : controls name <br>
188      * value : {@link ControlMigrator} used to take actions in order to migrate the control into a field.
189      */
190     protected void addCustomControlsToMigrate(HashMap<String, ControlMigrator> controlsToMigrate) {
191     }
192 
193     /**
194      * Register default actions to create on dialogs.
195      */
196     private void registerDialogActionToCreate(HashMap<String, List<ActionCreator>> customDialogActionsToMigrate) {
197         this.dialogActionsToMigrate = new HashMap<String, List<ActionCreator>>();
198         // Register default
199         // Save
200         ActionCreator saveAction = new BaseActionCreator("commit", "save changes", "info.magnolia.ui.admincentral.dialog.action.SaveDialogActionDefinition");
201         // Cancel
202         ActionCreator cancelAction = new BaseActionCreator("cancel", "cancel", "info.magnolia.ui.admincentral.dialog.action.CancelDialogActionDefinition");
203         // Create an entry
204         this.dialogActionsToMigrate.put(this.defaultDialogActions, Arrays.asList(saveAction, cancelAction));
205         // Register custom
206         if (customDialogActionsToMigrate != null) {
207             this.dialogActionsToMigrate.putAll(customDialogActionsToMigrate);
208         }
209     }
210 
211     /**
212      * Override this method in order to register custom actions to create on a specific dialog.<br>
213      *
214      * @param dialogActionsToMigrate
215      * key: Dialog name <br>
216      * value: List of {@link ActionCreator} to create on the desired dialog.
217      */
218     protected void addCustomDialogActionToCreate(HashMap<String, List<ActionCreator>> dialogActionsToMigrate) {
219     }
220 
221     /**
222      * Handle and Migrate a Dialog node.
223      */
224     private void performDialogMigration(Node dialog) throws RepositoryException {
225         // Get child Nodes (should be Tab)
226         Iterable<Node> tabNodes = NodeUtil.getNodes(dialog, DIALOG_FILTER);
227         if (tabNodes.iterator().hasNext()) {
228             // Check if it's a tab definition
229             if (dialog.hasProperty("controlType") && dialog.getProperty("controlType").getString().equals("tab")) {
230                 handleTab(dialog);
231             } else {
232                 // Handle action
233                 if (!dialog.hasProperty("controlType") && !dialog.hasProperty(PROPERTY_NAME_EXTENDS) && !dialog.hasProperty(PROPERTY_NAME_REFERENCE)) {
234                     handleAction(dialog);
235                 }
236                 // Handle tab
237                 handleTabs(dialog, tabNodes.iterator());
238             }
239             // Remove class property defined on Dialog level
240             if (dialog.hasProperty("class")) {
241                 dialog.getProperty("class").remove();
242             }
243         } else {
244             // Handle as a field.
245             handleField(dialog);
246         }
247 
248         handleExtendsAndReference(dialog);
249     }
250 
251     /**
252      * Add action to node.
253      */
254     private void handleAction(Node dialog) throws RepositoryException {
255         // Create actions node
256         NodeUtil.createPath(dialog, "actions", NodeTypes.ContentNode.NAME);
257         Node actionsNode = dialog.getNode("actions");
258 
259         List<ActionCreator> actions = dialogActionsToMigrate.get(defaultDialogActions);
260         // Use the specific Actions list if defined
261         if (dialogActionsToMigrate.containsKey(dialog.getName())) {
262             actions = dialogActionsToMigrate.get(dialog.getName());
263         }
264 
265         for (ActionCreator action : actions) {
266             action.create(actionsNode);
267         }
268 
269     }
270 
271     /**
272      * Handle Tabs.
273      */
274     private void handleTabs(Node dialog, Iterator<Node> tabNodes) throws RepositoryException {
275         Node form = NodeUtil.createPath(dialog, "form", NodeTypes.ContentNode.NAME);
276         handleFormLabels(dialog, form);
277         Node dialogTabs = NodeUtil.createPath(form, "tabs", NodeTypes.ContentNode.NAME);
278         while (tabNodes.hasNext()) {
279             Node tab = tabNodes.next();
280             // Handle Fields Tab
281             handleTab(tab);
282             // Move tab
283             NodeUtil.moveNode(tab, dialogTabs);
284         }
285     }
286 
287     /**
288      * Move the label property from dialog to form node.
289      */
290     private void handleFormLabels(Node dialog, Node form) throws RepositoryException {
291         moveAndRenameLabelProperty(dialog, form, "label");
292         moveAndRenameLabelProperty(dialog, form, "i18nBasename");
293         moveAndRenameLabelProperty(dialog, form, "description");
294     }
295 
296     /**
297      * Move the desired property if present from the source to the target node.
298      */
299     private void moveAndRenameLabelProperty(Node source, Node target, String propertyName) throws RepositoryException {
300         if (source.hasProperty(propertyName)) {
301             Property dialogProperty = source.getProperty(propertyName);
302             target.setProperty(propertyName, dialogProperty.getString());
303             dialogProperty.remove();
304         }
305     }
306 
307     /**
308      * Handle a Tab.
309      */
310     private void handleTab(Node tab) throws RepositoryException {
311         if (tab.hasProperty("controlType") && StringUtils.equals(tab.getProperty("controlType").getString(), "tab")) {
312             // Remove controlType Property
313             tab.getProperty("controlType").remove();
314         }
315 
316         // get all controls to be migrated
317         final Iterator<Node> controls = NodeUtil.getNodes(tab, NodeTypes.ContentNode.NAME).iterator();
318         if (controls.hasNext()) {
319             // create a fields Node
320             final Node fields = NodeUtil.createPath(tab, "fields", NodeTypes.ContentNode.NAME);
321 
322             while (controls.hasNext()) {
323                 final Node control = controls.next();
324                 // Handle fields
325                 handleField(control);
326                 // Move to fields
327                 NodeUtil.moveNode(control, fields);
328             }
329         }
330 
331         // Handle inheritable
332         handleExtendsAndReference(tab.hasNode("inheritable") ? tab.getNode("inheritable") : tab);
333     }
334 
335     /**
336      * Change controlType to the equivalent class.
337      * Change the extend path.
338      */
339     private void handleField(Node fieldNode) throws RepositoryException {
340         if (fieldNode.hasProperty("controlType")) {
341             String controlTypeName = fieldNode.getProperty("controlType").getString();
342 
343             if (controlsToMigrate.containsKey(controlTypeName)) {
344                 ControlMigrator controlMigration = controlsToMigrate.get(controlTypeName);
345                 controlMigration.migrate(fieldNode, installContext);
346             } else {
347                 fieldNode.setProperty("class", StaticFieldDefinition.class.getName());
348                 if (!fieldNode.hasProperty("value")) {
349                     fieldNode.setProperty("value", "Field not yet supported");
350                 }
351                 log.warn("No field defined for control '{}' for node '{}'", controlTypeName, fieldNode.getPath());
352             }
353         }
354         // Handle Field Extends/Reference
355         handleExtendsAndReference(fieldNode);
356     }
357 
358     private void handleExtendsAndReference(Node node) throws RepositoryException {
359         if (node.hasProperty("extends")) {
360             // Handle Field Extends
361             extendsAndReferenceProperty.add(node.getProperty(PROPERTY_NAME_EXTENDS));
362         } else if (node.hasProperty("reference")) {
363             // Handle Field Extends
364             extendsAndReferenceProperty.add(node.getProperty(PROPERTY_NAME_REFERENCE));
365         }
366     }
367 
368     /**
369      * Create a specific node filter.
370      */
371     private static AbstractPredicate<Node> DIALOG_FILTER = new AbstractPredicate<Node>() {
372         @Override
373         public boolean evaluateTyped(Node node) {
374             try {
375                 return !node.getName().startsWith(NodeTypes.JCR_PREFIX)
376                         && !NodeUtil.isNodeType(node, NodeTypes.MetaData.NAME) &&
377                         NodeUtil.isNodeType(node, NodeTypes.ContentNode.NAME);
378             } catch (RepositoryException e) {
379                 return false;
380             }
381         }
382     };
383 
384     /**
385      * Check if the extends and reference are correct. If not try to do the best
386      * to found a correct path.
387      */
388     private void postProcessForExtendsAndReference() throws RepositoryException {
389         for (Property property : extendsAndReferenceProperty) {
390             String path = property.getString();
391             if (path.equals("override")) {
392                 continue;
393             }
394             if (!isAbsolutePath(property, path)) {
395                 try {
396                     String newPath = property.getNode().getNode(path).getPath();
397                     property.setValue(newPath);
398                     log.info("Updated extends from parent relative path of node '{}' with original path '{}' by new path '{}'.", property.getPath(), path, newPath);
399                     path = newPath;
400                 } catch (RepositoryException re) {
401                     log.warn("Reference from propertyName '{}' to '{}' is an relative path and could not be linked. The initial value will be keeped", property.getPath(), path);
402                     continue;
403                 }
404             }
405 
406             if (!property.getSession().nodeExists(path)) {
407 
408                 String newPath = insertBeforeLastSlashAndTest(property.getSession(), path, "/tabs", "/fields", "/tabs/fields", "/form/tabs");
409                 if (newPath != null) {
410                     property.setValue(newPath);
411                     continue;
412                 }
413 
414                 // try to add a tabs before the 2nd last /
415                 String begin = path.substring(0, path.lastIndexOf("/"));
416                 String end = path.substring(begin.lastIndexOf("/"));
417                 begin = begin.substring(0, begin.lastIndexOf("/"));
418                 newPath = begin + "/form/tabs" + end;
419                 if (property.getSession().nodeExists(newPath)) {
420                     property.setValue(newPath);
421                     continue;
422                 }
423                 // try with a fields before the last / with a tabs before the 2nd last /
424                 newPath = insertBeforeLastSlash(newPath, "/fields");
425                 if (property.getSession().nodeExists(newPath)) {
426                     property.setValue(newPath);
427                 } else {
428                     log.warn("Reference from propertyName '{}' to '{}' not found. The initial value will be keeped", property.getPath(), newPath);
429                 }
430             }
431         }
432     }
433 
434     /**
435      * @return true if the path refers to an absolute path, false otherwise.
436      */
437     private boolean isAbsolutePath(Property property, String path) {
438         try {
439             property.getSession().nodeExists(path);
440             return true;
441         } catch (RepositoryException e) {
442             return false;
443         }
444     }
445 
446     /**
447      * Test insertBeforeLastSlash() for all toInserts.
448      * If newPath exist as a node return it.
449      */
450     private String insertBeforeLastSlashAndTest(Session session, String reference, String... toInserts) throws RepositoryException {
451         String res = null;
452         for (String toInsert : toInserts) {
453             String newPath = insertBeforeLastSlash(reference, toInsert);
454             if (session.nodeExists(newPath)) {
455                 return newPath;
456             }
457         }
458         return res;
459     }
460 
461     /**
462      * Insert the toInsert ("/tabs") before the last /.
463      */
464     private String insertBeforeLastSlash(String reference, String toInsert) {
465         String begin = reference.substring(0, reference.lastIndexOf("/"));
466         String end = reference.substring(reference.lastIndexOf("/"));
467         return begin + toInsert + end;
468     }
469 
470     /**
471      * Get All nodes defining an extends or reference property.
472      * If the property value refers to a relative path, <br>
473      * - try to resolve this path <br>
474      * - if the equivalent absolute path is found, replace in the property value the relative path by the absolute path.
475      */
476     private void resolveRelativeExtendsPath(Node dialog) {
477         try {
478             final String queryString = String.format("SELECT * FROM [nt:base] AS t WHERE ISDESCENDANTNODE(t, '%s') AND (%s is not null OR %s is not null)", dialog.getPath(), PROPERTY_NAME_EXTENDS, PROPERTY_NAME_REFERENCE);
479             final NodeIterator iterator = QueryUtil.search(RepositoryConstants.CONFIG, queryString, Query.JCR_SQL2);
480             while (iterator.hasNext()) {
481                 final Node node = iterator.nextNode();
482                 String propertyValue = PropertyUtil.getString(node, PROPERTY_NAME_EXTENDS);
483 
484                 Property property;
485                 if (StringUtils.isNotBlank(propertyValue)) {
486                     property = node.getProperty(PROPERTY_NAME_EXTENDS);
487                 } else {
488                     propertyValue = PropertyUtil.getString(node, PROPERTY_NAME_REFERENCE);
489                     property = node.getProperty(PROPERTY_NAME_REFERENCE);
490                 }
491 
492                 // Check if it's a relative path
493                 if (StringUtils.isNotBlank(propertyValue) && !isAbsolutePath(property, propertyValue) && node.hasNode(propertyValue)) {
494                     property.setValue(node.getNode(propertyValue).getPath());
495                     log.info("Change propertyValue of '{}' from '{}' to '{}'", property.getPath(), propertyValue, node.getNode(propertyValue).getPath());
496                 }
497             }
498         } catch (RepositoryException e) {
499             log.warn("Could not handle extends/reference property for the following definition ", NodeUtil.getNodePathIfPossible(dialog));
500         }
501     }
502 
503 }