View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.setup.nodetype;
35  
36  import info.magnolia.module.InstallContext;
37  import info.magnolia.module.delta.AbstractRepositoryTask;
38  import info.magnolia.module.delta.TaskExecutionException;
39  
40  import java.util.ArrayList;
41  import java.util.Collection;
42  import java.util.HashMap;
43  import java.util.Iterator;
44  import java.util.LinkedHashMap;
45  import java.util.List;
46  import java.util.Set;
47  
48  import javax.jcr.NamespaceException;
49  import javax.jcr.RepositoryException;
50  import javax.jcr.nodetype.NodeTypeDefinition;
51  import javax.jcr.nodetype.NodeTypeManager;
52  import javax.jcr.nodetype.NodeTypeTemplate;
53  
54  import org.apache.commons.lang3.StringUtils;
55  import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
56  import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
57  import org.apache.jackrabbit.spi.Name;
58  import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
59  import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import com.google.common.collect.Lists;
64  
65  /**
66   * Task allowing to register or un-register nodeTypes.
67   * <br>
68   * <br>
69   * <b>For Registration</b>
70   * <ul>
71   *     <li>override {@link #getNodeTypesToRegister(NodeTypeManager)}.</li>
72   *     <li>Every {@link NodeTypeDefinition} in the list will either be created if not present yet, otherwise the existing one will be updated.</li>
73   * </ul>
74   * <b>Warning:</b> Be careful with the order of the list of {@link NodeTypeDefinition}:<br>
75   * Always put the parent NodeType or newly referenced NodeTypes first. For example, you may want to create a new NodeType A
76   * that is referenced as super by an updated NodeType B, the list should be [A,B] so A is created before B. Otherwise,
77   * A will not be updated. In this case, the NodeType A will not be registered and a warn log is displayed.
78   * <br>
79   * <br>
80   * <b>For Un-registration</b>
81   * <ul>
82   *     <li>override {@link #getNodeTypesToUnregister(NodeTypeManager)}.</li>
83   * </ul>
84   * <b>Warning:</b> A NodeType will be unregistered only if no dependency issue is found<br>
85   * Potential dependency issues:<br>
86   * <ul>
87   *     <li>NodeType is referenced as child node by one ore more other NodeType definitions</li>
88   *     <li>NodeType is defined as super of one ore more other NodeType definitions</li>
89   * </ul>
90   * In this case, the NodeType will not be unregistered and a warn log is displayed.
91   * <br>
92   * <br>
93   * <b>Note:</b>  Most of the custom implementation used in this task is due to jackrabbit not being able to update
94   * NodeTypeDefinitions with non-trivial changes. Originally the intention was to keep it independent from jackrabbits API
95   * as well, but in reality we are already heavily dependant on jackrabbit API.
96   *
97   */
98  public abstract class AbstractNodeTypeRegistrationTask extends AbstractRepositoryTask {
99  
100     private final Logger log = LoggerFactory.getLogger(AbstractNodeTypeRegistrationTask.class);
101 
102     private String workspaceName;
103 
104     public AbstractNodeTypeRegistrationTask(String name, String description, String workspaceName) {
105         super(name, description);
106         this.workspaceName = workspaceName;
107     }
108 
109     @Override
110     public void doExecute(InstallContext installContext) throws RepositoryException, TaskExecutionException {
111         if (StringUtils.isBlank(workspaceName)) {
112             log.warn("No workspace defined. This task will resume");
113             return;
114         }
115 
116         NodeTypeManager nodeTypeManager = installContext.getJCRSession(workspaceName).getWorkspace().getNodeTypeManager();
117 
118         // Register nodeTypes
119         List<NodeTypeDefinition> nodeTypeToRegister = getNodeTypesToRegister(nodeTypeManager);
120         if (nodeTypeToRegister == null || nodeTypeToRegister.isEmpty()) {
121             log.info("No NodeType defined to register for the following workspace '{}'", workspaceName);
122         } else {
123             // exception not caught, installation will bail
124             registerNodeTypes(nodeTypeManager, nodeTypeToRegister, installContext);
125         }
126         // Un-register nodeTypes
127         List<String> nodeTypeToUnregister = getNodeTypesToUnregister(nodeTypeManager);
128         if (nodeTypeToUnregister == null || nodeTypeToUnregister.isEmpty()) {
129             log.info("No NodeType defined to unregister for the following workspace '{}'", workspaceName);
130         } else {
131             try {
132                 unregisterNodeTypes(nodeTypeManager, nodeTypeToUnregister);
133             } catch (RepositoryException e) {
134                 installContext.warn("Un-registration of NodeTypes generated exceptions. NodeType will not be Unregistered. Please check the NodeType dependency.");
135                 log.warn("Not able to un-register the following node type {}. The original definition is kept.", nodeTypeToUnregister, e);
136             }
137         }
138     }
139 
140     /**
141      * Get a List of {@link NodeTypeDefinition} to register or update (in case they are already created).
142      */
143     public abstract List<NodeTypeDefinition> getNodeTypesToRegister(NodeTypeManager nodeTypeManager) throws RepositoryException;
144 
145     /**
146      * Get a List of {@link NodeTypeDefinition#getName()} to un-register.
147      */
148     public abstract List<String> getNodeTypesToUnregister(NodeTypeManager nodeTypeManager) throws RepositoryException;
149 
150     /**
151      * Register a list of NodeTypes.
152      *   <ul>
153      *     <li>In case of new NodeType definition, simply register it.</li>
154      *     <li>In case the NodeType definition already exist:
155      *       <ul>
156      *         <li>Get all NodeType definitions that have a dependency to the current definition.</li>
157      *         <li>Un-register the complete list of dependent NodeTypes (including the current one).</li>
158      *         <li>Re-Register the complete list of dependent NodeTypes, including the new NodeType definition.</li>
159      *         <li>In case of exception, re register the original NodeType definition and including the dependencies  andlog the exception.</li>
160      *       </ul>
161      *     </li>
162      *   </ul>
163      */
164     private void registerNodeTypes(NodeTypeManager nodeTypeManager, List<NodeTypeDefinition> nodeTypesToRegister, InstallContext installContext) throws RepositoryException {
165         NamePathResolver namePathResolver = ((NodeTypeManagerImpl) nodeTypeManager).getNamePathResolver();
166         // Iterate the nodeTypeToRegister List
167         for (NodeTypeDefinition nodeType : nodeTypesToRegister) {
168             // NodeType already registered, Update (unregister, register)
169             String nodeTypeName = nodeType.getName();
170 
171             if (nodeTypeManager.hasNodeType(nodeTypeName)) {
172 
173                 // Get the original NodeType definition. Useful in case of dependency issue (the original NodeType is re-registered in case of exception).
174                 NodeTypeDefinition originalNodeType = nodeTypeManager.getNodeType(nodeTypeName);
175 
176                 // UN-REGISTRATION
177                 List<NodeTypeDefinition> dependentNodeTypeDefinitions = Lists.newArrayList();
178                 getDependencies(nodeTypeManager, nodeTypeName, dependentNodeTypeDefinitions);
179                 dependentNodeTypeDefinitions.add(nodeType);
180                 List<String> nodeTypeNames = getNodeTypeNames(namePathResolver, dependentNodeTypeDefinitions);
181                 List<String> dependentNodeTypeNames = new ArrayList<>(nodeTypeNames);
182                 dependentNodeTypeNames.remove(nodeTypeName);
183                 unregisterNodeTypes(nodeTypeManager, nodeTypeNames);
184                 log.info("In order to unregister '{}', the following depending node types have been unregistered first: {}.", nodeTypeName, dependentNodeTypeNames);
185                 dependentNodeTypeDefinitions.remove(nodeType);
186 
187                 // RE-REGISTRATION
188                 log.info("Registering '{}' and the following depending nodeTypes: {}", nodeTypeName, dependentNodeTypeNames);
189                 try {
190                     registerNodeType(nodeTypeManager, nodeType);
191                 } catch (RepositoryException e) {
192                     // Register the previous version of the NodeType, if this fails the task will bail
193                     registerNodeType(nodeTypeManager, originalNodeType);
194                     String error = String.format("Registration (update of an existing NodeType) of the following NodeType generated exceptions '%s'. " +
195                             "The original version of the definition is kept. Please check the NodeType dependency and the input list order.", nodeTypeName);
196                     installContext.warn(error);
197                     log.warn("Not able to register the following node type {}. The original definition is kept.", nodeTypeName, e);
198                 }
199 
200                 for (NodeTypeDefinition nodeTypeDefinition : dependentNodeTypeDefinitions) {
201                     try {
202                         registerNodeType(nodeTypeManager, nodeTypeDefinition);
203                     } catch (RepositoryException e) {
204                         String error = String.format("Re-registration of the NodeType '%s' as a dependent nodeType of '%s' failed.", nodeTypeDefinition.getName(), nodeTypeName);
205                         installContext.warn(error);
206                         log.error(error, e);
207                     }
208                 }
209             } else {
210                 // NodeType is new, Create (register)
211                 nodeTypeManager.registerNodeType(nodeType, false);
212                 log.info("Registering new '{}' node type.", nodeTypeName);
213             }
214         }
215     }
216 
217     /**
218      * As Jackrabbit {@link NodeTypeRegistry#unregisterNodeTypes} doesn't cover more
219      * than one level of dependencies, we need to manually get all dependencies recursively. The ones we find are added
220      * to a list in order to register them afterwards.
221      */
222     private void getDependencies(NodeTypeManager nodeTypeManager, String nodeTypeName, List<NodeTypeDefinition> dependentNodeTypeDefinitions) throws RepositoryException {
223         NodeTypeRegistry registry = ((NodeTypeManagerImpl) nodeTypeManager).getNodeTypeRegistry();
224         NamePathResolver namePathResolver = ((NodeTypeManagerImpl) nodeTypeManager).getNamePathResolver();
225         Set<Name> directChildNodeTypeQNames = registry.getDependentNodeTypes(namePathResolver.getQName(nodeTypeName));
226 
227         if (directChildNodeTypeQNames.size() == 0) {
228             return;
229         }
230 
231         for (Name directChildNodeTypeQName : directChildNodeTypeQNames) {
232             dependentNodeTypeDefinitions.add(((NodeTypeManagerImpl) nodeTypeManager).getNodeType(directChildNodeTypeQName));
233             getDependencies(nodeTypeManager, namePathResolver.getJCRName(directChildNodeTypeQName), dependentNodeTypeDefinitions);
234         }
235     }
236 
237     /**
238      * Register the {@link NodeTypeDefinition}.
239      */
240     private void registerNodeType(NodeTypeManager nodeTypeManager, NodeTypeDefinition nodeTypeDefinition) throws RepositoryException {
241         // "transforming" each definition into a template, so that nodeTypeManager.registerNodeType doesn't barf unnecessary exceptions.
242         final NodeTypeTemplate nodeTypeTemplate = nodeTypeManager.createNodeTypeTemplate(nodeTypeDefinition);
243         log.debug("Registering the following '{}' node type.", nodeTypeTemplate.getName());
244         nodeTypeManager.registerNodeType(nodeTypeTemplate, true);
245     }
246 
247     /**
248      * Un-register all nodeTypes passed as {@code nodeTypeNames}. Filter the list as the {@link NodeTypeManager#unregisterNodeTypes(String[])}
249      * throws Exception if the nodeType is not registered.
250      */
251     private void unregisterNodeTypes(final NodeTypeManager nodeTypeManager, List<String> nodeTypeNames) throws RepositoryException {
252         Iterator<String> it = nodeTypeNames.iterator();
253         while (it.hasNext()) {
254             String nodeTypeName = it.next();
255             if (!nodeTypeManager.hasNodeType(nodeTypeName)) {
256                 log.warn("Node type '{}' is not registered . No un-registration will be performed for this nodeType.", nodeTypeName);
257                 it.remove();
258             }
259         }
260 
261         NodeTypeRegistry.disableCheckForReferencesInContentException = true;
262         nodeTypeManager.unregisterNodeTypes(nodeTypeNames.toArray(new String[nodeTypeNames.size()]));
263         log.info("Unregistered the following NodeTypes '{}'.", nodeTypeNames);
264         NodeTypeRegistry.disableCheckForReferencesInContentException = false;
265     }
266 
267     /**
268      * Transform a list of {@link NodeTypeDefinition}s to an array-list of their names.
269      */
270     private List<String> getNodeTypeNames(NamePathResolver namePathResolver, Collection<NodeTypeDefinition> nodeTypeDefinitions) throws NamespaceException, IllegalNameException {
271         List<String> names = new ArrayList<>();
272 
273         for (NodeTypeDefinition nodeTypeDefinition : nodeTypeDefinitions) {
274             Name qName = namePathResolver.getQName(nodeTypeDefinition.getName());
275             names.add(namePathResolver.getJCRName(qName));
276         }
277         return names;
278     }
279 
280     /**
281      * Create a Map containing all child NodeType of.<br>
282      * - key = NodeType name.<br>
283      * - value = {@link NodeTypeDefinition}.
284      *
285      * @deprecated since 5.4.3, use {@link NodeTypeRegistry#getDependentNodeTypes(Name)}. This function was added to keep
286      * the implementation jackrabbit-API agnostic. But we already have jackrabbit dependencies in the passed params, so
287      * it essentially is useless. Apart from that it was causing the migration to fail, as the returned Map did not retain
288      * the order of the nodeTypes.
289      */
290     @Deprecated
291     protected HashMap<String, NodeTypeDefinition> getDependentNodeTypes(NodeTypeDefinition nodeType, NodeTypeRegistry registry, NamePathResolver namePathResolver, NodeTypeManager nodeTypeManager) throws RepositoryException {
292 
293         HashMap<String, NodeTypeDefinition> dependentNodeType = new LinkedHashMap<>();
294         // Get the Name in order to get the Dependent Node type
295         Name qName = namePathResolver.getQName(nodeType.getName());
296         Set<Name> dependentNodesName = registry.getDependentNodeTypes(qName);
297         // for every dependent node type, call recursively in order to get all the dependency tree.
298         if (dependentNodesName != null && !dependentNodesName.isEmpty()) {
299             for (Name name : dependentNodesName) {
300                 dependentNodeType.putAll(getDependentNodeTypes(nodeTypeManager.getNodeType(namePathResolver.getJCRName(name)), registry, namePathResolver, nodeTypeManager));
301             }
302         }
303 
304         dependentNodeType.put(nodeType.getName(), nodeType);
305         return dependentNodeType;
306     }
307 }