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