View Javadoc

1   /**
2    * This file Copyright (c) 2012-2013 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.core.Path;
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.module.InstallContext;
43  import info.magnolia.module.delta.AbstractTask;
44  import info.magnolia.module.delta.TaskExecutionException;
45  import info.magnolia.repository.RepositoryConstants;
46  import info.magnolia.ui.dialog.setup.migration.CheckBoxRadioControlMigration;
47  import info.magnolia.ui.dialog.setup.migration.CheckBoxSwitchControlMigration;
48  import info.magnolia.ui.dialog.setup.migration.ControlMigration;
49  import info.magnolia.ui.dialog.setup.migration.DamControlMigration;
50  import info.magnolia.ui.dialog.setup.migration.DateControlMigration;
51  import info.magnolia.ui.dialog.setup.migration.EditControlMigration;
52  import info.magnolia.ui.dialog.setup.migration.FckEditControlMigration;
53  import info.magnolia.ui.dialog.setup.migration.FileControlMigration;
54  import info.magnolia.ui.dialog.setup.migration.HiddenControlMigration;
55  import info.magnolia.ui.dialog.setup.migration.LinkControlMigration;
56  import info.magnolia.ui.dialog.setup.migration.MultiSelectControlMigration;
57  import info.magnolia.ui.dialog.setup.migration.SelectControlMigration;
58  import info.magnolia.ui.dialog.setup.migration.StaticControlMigration;
59  
60  import java.io.File;
61  import java.io.FileInputStream;
62  import java.io.FileOutputStream;
63  import java.io.IOException;
64  import java.util.HashMap;
65  import java.util.HashSet;
66  import java.util.Iterator;
67  
68  import javax.jcr.ImportUUIDBehavior;
69  import javax.jcr.Node;
70  import javax.jcr.Property;
71  import javax.jcr.RepositoryException;
72  import javax.jcr.Session;
73  
74  import org.apache.commons.io.IOUtils;
75  import org.apache.commons.lang.StringUtils;
76  import org.slf4j.Logger;
77  import org.slf4j.LoggerFactory;
78  
79  /**
80   * Dialog migration main task.
81   * Migrate dialogs for the specified moduleName.
82   */
83  public class DialogMigrationTask extends AbstractTask {
84  
85      private static final Logger log = LoggerFactory.getLogger(DialogMigrationTask.class);
86      private final String moduleName;
87      private final HashSet<Property> extendsAndReferenceProperty = new HashSet<Property>();
88      private HashMap<String, ControlMigration> controlMigrationMap;
89  
90      public DialogMigrationTask(String moduleName) {
91          super("Dialog Migration for 5.x", "Migrate dialog for the following module: " + moduleName);
92          this.moduleName = moduleName;
93          this.controlMigrationMap = new HashMap<String, ControlMigration>();
94      }
95  
96      /**
97       * Handle all Dialogs registered and migrate them.
98       */
99      @Override
100     public void execute(InstallContext installContext) throws TaskExecutionException {
101         try {
102             this.controlMigrationMap = getCustomMigrationTask();
103             String dialogNodeName = "dialogs";
104             String dialogPath = "/modules/" + moduleName + "/" + dialogNodeName;
105 
106             Session session = installContext.getJCRSession(RepositoryConstants.CONFIG);
107             Node dialog = session.getNode(dialogPath);
108             // Copy to Dialog50
109             String newDialogPath = dialog.getPath() + "50";
110             copyInSession(dialog, newDialogPath);
111             NodeUtil.visit(dialog, new NodeVisitor() {
112                 @Override
113                 public void visit(Node current) throws RepositoryException {
114                     for (Node dialogNode : NodeUtil.getNodes(current, NodeTypes.ContentNode.NAME)) {
115                         performDialogMigration(dialogNode);
116                     }
117                 }
118             }, new NodeTypePredicate(NodeTypes.Content.NAME));
119             session.removeItem(dialogPath);
120             session.move(newDialogPath, dialogPath);
121 
122             // Try to resolve references for extends.
123             postProcessForExtendsAndReference();
124 
125         } catch (Exception e) {
126             log.error("", e);
127             installContext.warn("Could not Migrate Dialog for the following module " + moduleName);
128         }
129     }
130 
131     /**
132      * Initialize the map used to migrate controls to Fields.<br>
133      * Each control is link to a specific {@link ControlMigration}.
134      * <b>Override this class in order to add your own controls migration task.</b>
135      */
136     protected HashMap<String, ControlMigration> getCustomMigrationTask() {
137         HashMap<String, ControlMigration> customMigrationTask = new HashMap<String, ControlMigration>();
138         customMigrationTask.put("edit", new EditControlMigration());
139         customMigrationTask.put("fckEdit", new FckEditControlMigration());
140         customMigrationTask.put("date", new DateControlMigration());
141         customMigrationTask.put("select", new SelectControlMigration());
142         customMigrationTask.put("checkbox", new CheckBoxRadioControlMigration(true));
143         customMigrationTask.put("checkboxSwitch", new CheckBoxSwitchControlMigration());
144         customMigrationTask.put("radio", new CheckBoxRadioControlMigration(false));
145         customMigrationTask.put("dam", new DamControlMigration());
146         customMigrationTask.put("uuidLink", new LinkControlMigration());
147         customMigrationTask.put("link", new LinkControlMigration());
148         customMigrationTask.put("categorizationUUIDMultiSelect", new MultiSelectControlMigration(true));
149         customMigrationTask.put("multiselect", new MultiSelectControlMigration(false));
150         customMigrationTask.put("file", new FileControlMigration());
151         customMigrationTask.put("static", new StaticControlMigration());
152         customMigrationTask.put("hidden", new HiddenControlMigration());
153 
154         return customMigrationTask;
155     }
156 
157     /**
158      * Handle and Migrate a Dialog node.
159      */
160     private void performDialogMigration(Node dialog) throws RepositoryException {
161         // Get child Nodes (should be Tab)
162         Iterable<Node> tabNodes = NodeUtil.getNodes(dialog, DIALOG_FILTER);
163         if (tabNodes.iterator().hasNext()) {
164             // Check if it's a tab definition
165             if (dialog.hasProperty("controlType") && dialog.getProperty("controlType").getString().equals("tab")) {
166                 handleTab(dialog);
167             } else {
168                 // Handle action
169                 if (!dialog.hasProperty("controlType") && !dialog.hasProperty("extends") && !dialog.hasProperty("reference")) {
170                     handleAction(dialog);
171                 }
172                 // Handle tab
173                 handleTabs(dialog, tabNodes.iterator());
174             }
175             // Remove class property defined on Dialog level
176             if (dialog.hasProperty("class")) {
177                 dialog.getProperty("class").remove();
178             }
179         } else {
180             // Handle as a field.
181             handleField(dialog);
182         }
183 
184         handleExtendsAndReference(dialog);
185     }
186 
187     /**
188      * Add action to node.
189      */
190     private void handleAction(Node dialog) throws RepositoryException {
191         dialog.addNode("actions", NodeTypes.ContentNode.NAME);
192 
193         Node node = dialog.getNode("actions");
194 
195         node.addNode("commit", NodeTypes.ContentNode.NAME).setProperty("label", "save changes");
196         node.addNode("cancel", NodeTypes.ContentNode.NAME).setProperty("label", "cancel");
197         node.getNode("commit").setProperty("class", "info.magnolia.ui.admincentral.dialog.action.SaveDialogActionDefinition");
198         node.getNode("cancel").setProperty("class", "info.magnolia.ui.admincentral.dialog.action.CancelDialogActionDefinition");
199 
200     }
201 
202     /**
203      * Handle Tabs.
204      */
205     private void handleTabs(Node dialog, Iterator<Node> tabNodes) throws RepositoryException {
206         Node form = dialog.addNode("form", NodeTypes.ContentNode.NAME);
207         handleFormLabels(dialog, form);
208         Node dialogTabs = form.addNode("tabs", NodeTypes.ContentNode.NAME);
209         while (tabNodes.hasNext()) {
210             Node tab = tabNodes.next();
211             // Handle Fields Tab
212             handleTab(tab);
213             // Move tab
214             NodeUtil.moveNode(tab, dialogTabs);
215         }
216     }
217 
218     /**
219      * Move the label property from dialog to form node.
220      */
221     private void handleFormLabels(Node dialog, Node form) throws RepositoryException {
222         moveAndRenameLabelProperty(dialog, form, "label");
223         moveAndRenameLabelProperty(dialog, form, "i18nBasename");
224         moveAndRenameLabelProperty(dialog, form, "description");
225     }
226 
227     /**
228      * Move the desired property if present from the source to the target node.
229      */
230     private void moveAndRenameLabelProperty(Node source, Node target, String propertyName) throws RepositoryException {
231         if (source.hasProperty(propertyName)) {
232             Property dialogProperty = source.getProperty(propertyName);
233             target.setProperty(propertyName, dialogProperty.getString());
234             dialogProperty.remove();
235         }
236     }
237 
238     /**
239      * Handle a Tab.
240      */
241     private void handleTab(Node tab) throws RepositoryException {
242         if ((tab.hasProperty("controlType") && StringUtils.equals(tab.getProperty("controlType").getString(), "tab")) || (tab.getParent().hasProperty("extends"))) {
243             if (tab.hasProperty("controlType") && StringUtils.equals(tab.getProperty("controlType").getString(), "tab")) {
244                 // Remove controlType Property
245                 tab.getProperty("controlType").remove();
246             }
247             // get all controls to be migrated
248             Iterator<Node> controls = NodeUtil.getNodes(tab, NodeTypes.ContentNode.NAME).iterator();
249             // create a fields Node
250             Node fields = tab.addNode("fields", NodeTypes.ContentNode.NAME);
251 
252             while (controls.hasNext()) {
253                 Node control = controls.next();
254                 // Handle fields
255                 handleField(control);
256                 // Move to fields
257                 NodeUtil.moveNode(control, fields);
258             }
259         } else if (tab.hasNode("inheritable")) {
260             // Handle inheritable
261             Node inheritable = tab.getNode("inheritable");
262             handleExtendsAndReference(inheritable);
263         } else {
264             handleExtendsAndReference(tab);
265         }
266     }
267 
268     /**
269      * Change controlType to the equivalent class.
270      * Change the extend path.
271      */
272     private void handleField(Node fieldNode) throws RepositoryException {
273         if (fieldNode.hasProperty("controlType")) {
274             String controlTypeName = fieldNode.getProperty("controlType").getString();
275 
276             if (controlMigrationMap.containsKey(controlTypeName)) {
277                 ControlMigration controlMigration = controlMigrationMap.get(controlTypeName);
278                 controlMigration.migrate(fieldNode);
279             } else {
280                 fieldNode.setProperty("class", "info.magnolia.ui.form.field.definition.StaticFieldDefinition");
281                 if (!fieldNode.hasProperty("value")) {
282                     fieldNode.setProperty("value", "Field not yet supported");
283                 }
284                 log.warn("No dialog define for control '{}' for node '{}'", controlTypeName, fieldNode.getPath());
285             }
286         } else {
287             // Handle Field Extends/Reference
288             handleExtendsAndReference(fieldNode);
289         }
290     }
291 
292 
293     private void handleExtendsAndReference(Node node) throws RepositoryException {
294         if (node.hasProperty("extends")) {
295             // Handle Field Extends
296             extendsAndReferenceProperty.add(node.getProperty("extends"));
297         } else if (node.hasProperty("reference")) {
298             // Handle Field Extends
299             extendsAndReferenceProperty.add(node.getProperty("reference"));
300         }
301     }
302 
303     /**
304      * Create a specific node filter.
305      */
306     private static AbstractPredicate<Node> DIALOG_FILTER = new AbstractPredicate<Node>() {
307         @Override
308         public boolean evaluateTyped(Node node) {
309             try {
310                 return !node.getName().startsWith(NodeTypes.JCR_PREFIX)
311                         && !NodeUtil.isNodeType(node, NodeTypes.MetaData.NAME) &&
312                         NodeUtil.isNodeType(node, NodeTypes.ContentNode.NAME);
313             } catch (RepositoryException e) {
314                 return false;
315             }
316         }
317     };
318 
319     /**
320      * Check if the extends and reference are correct. If not try to do the best
321      * to found a correct path.
322      */
323     private void postProcessForExtendsAndReference() throws RepositoryException {
324         for (Property p : extendsAndReferenceProperty) {
325             String path = p.getString();
326             if (path.equals("override")) {
327                 continue;
328             }
329             if (!p.getSession().nodeExists(path)) {
330 
331                 String newPath = insertBeforeLastSlashAndTest(p.getSession(), path, "/tabs", "/fields", "/tabs/fields", "/form/tabs");
332                 if (newPath != null) {
333                     p.setValue(newPath);
334                     continue;
335                 }
336 
337                 // try to add a tabs before the 2nd last /
338                 String beging = path.substring(0, path.lastIndexOf("/"));
339                 String end = path.substring(beging.lastIndexOf("/"));
340                 beging = beging.substring(0, beging.lastIndexOf("/"));
341                 newPath = beging + "/form/tabs" + end;
342                 if (p.getSession().nodeExists(newPath)) {
343                     p.setValue(newPath);
344                     continue;
345                 }
346                 // try with a fields before the last / with a tabs before the 2nd last /
347                 newPath = insertBeforeLastSlash(newPath, "/fields");
348                 if (p.getSession().nodeExists(newPath)) {
349                     p.setValue(newPath);
350                 } else {
351                     log.warn("reference to " + path + " not found");
352                 }
353             }
354         }
355     }
356 
357     /**
358      * Test insertBeforeLastSlash() for all toInserts.
359      * If newPath exist as a node return it.
360      */
361     private String insertBeforeLastSlashAndTest(Session session, String reference, String... toInserts) throws RepositoryException {
362         String res = null;
363         for (String toInsert : toInserts) {
364             String newPath = insertBeforeLastSlash(reference, toInsert);
365             if (session.nodeExists(newPath)) {
366                 return newPath;
367             }
368         }
369         return res;
370     }
371 
372     /**
373      * Insert the toInsert ("/tabs") before the last /.
374      */
375     private String insertBeforeLastSlash(String reference, String toInsert) {
376         String beging = reference.substring(0, reference.lastIndexOf("/"));
377         String end = reference.substring(reference.lastIndexOf("/"));
378         return beging + toInsert + end;
379     }
380 
381     /**
382      * Session based copy operation. As JCR only supports workspace based copies this operation is performed.
383      * by using export import operations.
384      */
385     private void copyInSession(Node src, String dest) throws RepositoryException {
386         final String destParentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(dest, "/"), "/");
387         final String destNodeName = StringUtils.substringAfterLast(dest, "/");
388         final Session session = src.getSession();
389         try {
390             final File file = File.createTempFile("mgnl", null, Path.getTempDirectory());
391             final FileOutputStream outStream = new FileOutputStream(file);
392             session.exportSystemView(src.getPath(), outStream, false, false);
393             outStream.flush();
394             IOUtils.closeQuietly(outStream);
395             FileInputStream inStream = new FileInputStream(file);
396             session.importXML(
397                     destParentPath,
398                     inStream,
399                     ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
400             IOUtils.closeQuietly(inStream);
401             file.delete();
402             if (!StringUtils.equals(src.getName(), destNodeName)) {
403                 String currentPath;
404                 if (destParentPath.equals("/")) {
405                     currentPath = "/" + src.getName();
406                 } else {
407                     currentPath = destParentPath + "/" + src.getName();
408                 }
409                 session.move(currentPath, dest);
410             }
411         } catch (IOException e) {
412             throw new RepositoryException("Can't copy node " + src + " to " + dest, e);
413         }
414     }
415 }