001/* 002 * Copyright 2011 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.repository; 017 018import javax.jcr.ItemExistsException; 019import javax.jcr.ItemNotFoundException; 020import javax.jcr.Node; 021import javax.jcr.NodeIterator; 022import javax.jcr.PathNotFoundException; 023import javax.jcr.Property; 024import javax.jcr.PropertyIterator; 025import javax.jcr.PropertyType; 026import javax.jcr.RepositoryException; 027import javax.jcr.Session; 028import javax.jcr.Value; 029import javax.jcr.version.VersionHistory; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.logger.AbstractLogEnabled; 033import org.apache.avalon.framework.thread.ThreadSafe; 034import org.apache.cocoon.util.HashUtil; 035import org.apache.commons.collections.Predicate; 036import org.apache.commons.collections.PredicateUtils; 037import org.apache.commons.lang.StringUtils; 038import org.apache.jackrabbit.JcrConstants; 039import org.apache.jackrabbit.core.NodeImpl; 040 041import org.ametys.cms.support.AmetysPredicateUtils; 042import org.ametys.plugins.repository.RepositoryConstants; 043import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory; 044import org.ametys.plugins.repository.jcr.NameHelper; 045 046/** 047 * Helper for common processing used while synchronizing. 048 */ 049public class CloneComponent extends AbstractLogEnabled implements Component, ThreadSafe 050{ 051 /** Avalon Role */ 052 public static final String ROLE = CloneComponent.class.getName(); 053 054 /** 055 * Adds a node to a parent node using a source node for name, type 056 * and potential UUID. 057 * @param srcNode the source node. 058 * @param parentNode the parent node for the newly created node. 059 * @param nodeName the node name to use. 060 * @return the created node. 061 * @throws RepositoryException if an error occurs. 062 */ 063 public Node addNodeWithUUID(Node srcNode, Node parentNode, String nodeName) throws RepositoryException 064 { 065 if (srcNode.isNodeType(JcrConstants.NT_FROZENNODE)) 066 { 067 String nodeTypeName = srcNode.getProperty(JcrConstants.JCR_FROZENPRIMARYTYPE).getString(); 068 String uuid = null; 069 070 if (srcNode.hasProperty(JcrConstants.JCR_FROZENUUID)) 071 { 072 uuid = srcNode.getProperty(JcrConstants.JCR_FROZENUUID).getString(); 073 } 074 075 if (uuid == null) 076 { 077 return parentNode.addNode(nodeName, nodeTypeName); 078 } 079 else 080 { 081 return ((NodeImpl) parentNode).addNodeWithUuid(nodeName, nodeTypeName, uuid); 082 } 083 } 084 else 085 { 086 if (!srcNode.isNodeType(JcrConstants.MIX_REFERENCEABLE)) 087 { 088 return parentNode.addNode(nodeName, srcNode.getPrimaryNodeType().getName()); 089 } 090 else 091 { 092 return ((NodeImpl) parentNode).addNodeWithUuid(nodeName, srcNode.getPrimaryNodeType().getName(), srcNode.getIdentifier()); 093 } 094 } 095 } 096 097 /** 098 * Clones all the properties of a node. 099 * @param srcNode the source node. 100 * @param clonedNode the cloned node. 101 * @param propertyPredicate the property selector. 102 * @throws RepositoryException if an error occurs. 103 */ 104 public void cloneAllProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException 105 { 106 // First remove existing matching properties from cloned Node 107 PropertyIterator clonedProperties = clonedNode.getProperties(); 108 while (clonedProperties.hasNext()) 109 { 110 Property property = clonedProperties.nextProperty(); 111 if (AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate).evaluate(property)) 112 { 113 property.remove(); 114 } 115 } 116 117 // Then copy properties 118 PropertyIterator itProperties = srcNode.getProperties(); 119 120 while (itProperties.hasNext()) 121 { 122 Property property = itProperties.nextProperty(); 123 124 // Ignore protected properties 125 if (AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate).evaluate(property)) 126 { 127 cloneProperty(clonedNode, property); 128 } 129 } 130 } 131 132 /** 133 * Clone a property. 134 * @param clonedNode the node to copy the property to. 135 * @param property the property to clone. 136 * @throws RepositoryException if an error occurs. 137 */ 138 protected void cloneProperty(Node clonedNode, Property property) throws RepositoryException 139 { 140 Session session = clonedNode.getSession(); 141 142 if (property.getType() == PropertyType.REFERENCE) 143 { 144 try 145 { 146 // Clone the referenced nodes. 147 if (property.isMultiple()) 148 { 149 Value[] sourceValues = property.getValues(); 150 Value[] clonedValues = new Value[sourceValues.length]; 151 152 for (int i = 0; i < sourceValues.length; i++) 153 { 154 Node referencedNode = session.getNodeByIdentifier(sourceValues[i].getString()); 155 156 Node clonedReferencedNode = null; 157 try 158 { 159 clonedReferencedNode = clonedNode.getSession().getNodeByIdentifier(referencedNode.getIdentifier()); 160 } 161 catch (ItemNotFoundException e) 162 { 163 clonedReferencedNode = getOrCloneNode(clonedNode.getSession(), referencedNode); 164 } 165 166 clonedValues[i] = session.getValueFactory().createValue(clonedReferencedNode); 167 } 168 169 clonedNode.setProperty(property.getName(), clonedValues); 170 } 171 else 172 { 173 Node referencedNode = property.getNode(); 174 175 Node clonedReferencedNode = null; 176 try 177 { 178 clonedReferencedNode = clonedNode.getSession().getNodeByIdentifier(referencedNode.getIdentifier()); 179 } 180 catch (ItemNotFoundException e) 181 { 182 clonedReferencedNode = getOrCloneNode(clonedNode.getSession(), referencedNode); 183 } 184 185 clonedNode.setProperty(property.getName(), clonedReferencedNode); 186 } 187 } 188 catch (ItemNotFoundException e) 189 { 190 // the target node does not exist anymore, this could be due to workflow having been deleted 191 } 192 } 193 else 194 { 195 if (property.getDefinition().isMultiple()) 196 { 197 clonedNode.setProperty(property.getName(), property.getValues(), property.getType()); 198 } 199 else 200 { 201 clonedNode.setProperty(property.getName(), property.getValue(), property.getType()); 202 } 203 } 204 } 205 206 /** 207 * Clones a node by preserving the source node UUID. 208 * @param srcNode the source node. 209 * @param clonedNode the cloned node. 210 * @param propertyPredicate the property selector. 211 * @param nodePredicate the node selector. 212 * @throws RepositoryException if an error occurs. 213 */ 214 public void cloneNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 215 { 216 // Clone properties 217 cloneAllProperties(srcNode, clonedNode, propertyPredicate); 218 219 // Remove all matching subNodes before cloning, for better handling of same name siblings 220 NodeIterator subNodes = clonedNode.getNodes(); 221 222 while (subNodes.hasNext()) 223 { 224 Node subNode = subNodes.nextNode(); 225 226 if (nodePredicate.evaluate(subNode)) 227 { 228 subNode.remove(); 229 } 230 } 231 232 // Then copy sub nodes 233 NodeIterator itNodes = srcNode.getNodes(); 234 235 while (itNodes.hasNext()) 236 { 237 Node subNode = itNodes.nextNode(); 238 239 if (nodePredicate.evaluate(subNode)) 240 { 241 Node clonedSubNode = addNodeWithUUID(subNode, clonedNode, subNode.getName()); 242 cloneNodeAndPreserveUUID(subNode, clonedSubNode, propertyPredicate, nodePredicate); 243 } 244 } 245 } 246 247 /** 248 * Get or clone a node. 249 * @param destSession the destination session. 250 * @param node the node in the source workspace. 251 * @return the cloned Node in the destination session. 252 * @throws RepositoryException if an error occurs. 253 */ 254 public Node getOrCloneNode(Session destSession, Node node) throws RepositoryException 255 { 256 return getOrCloneNode(destSession, node, null); 257 } 258 259 /** 260 * Get or clone a node in a specified version. 261 * @param destSession the destination session. 262 * @param node the node in the source workspace. 263 * @param version the node version to clone, null to get the current version. 264 * @return the cloned Node in the destination session. 265 * @throws RepositoryException if an error occurs. 266 */ 267 public Node getOrCloneNode(Session destSession, Node node, String version) throws RepositoryException 268 { 269 Node clonedParentNode = cloneAncestorsAndPreserveUUID(node, destSession); 270 String nodeName = node.getName(); 271 272 if (clonedParentNode.hasNode(nodeName)) 273 { 274 return clonedParentNode.getNode(nodeName); 275 } 276 else 277 { 278 Node clonedNode = null; 279 280 if (StringUtils.isNotEmpty(version)) 281 { 282 VersionHistory versionHistory = node.getSession().getWorkspace().getVersionManager().getVersionHistory(node.getPath()); 283 Node validatedNode = versionHistory.getVersionByLabel(version).getFrozenNode(); 284 clonedNode = addNodeWithUUID(validatedNode, clonedParentNode, nodeName); 285 286 cloneNodeAndPreserveUUID(validatedNode, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 287 } 288 else 289 { 290 clonedNode = addNodeWithUUID(node, clonedParentNode, nodeName); 291 292 cloneNodeAndPreserveUUID(node, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 293 } 294 295 return clonedNode; 296 } 297 } 298 299 /** 300 * Clones ancestors of a node by preserving the source node UUID. 301 * @param srcNode the source node. 302 * @param destSession the destination session. 303 * @return the parent node the destination workspace. 304 * @throws RepositoryException if an error occurs. 305 */ 306 public Node cloneAncestorsAndPreserveUUID(Node srcNode, Session destSession) throws RepositoryException 307 { 308 if (srcNode.getName().length() == 0) 309 { 310 // We are on the root node which already exists 311 return destSession.getRootNode(); 312 } 313 else 314 { 315 Node destRootNode = destSession.getRootNode(); 316 Node parentNode = srcNode.getParent(); 317 String parentNodePath = parentNode.getPath().substring(1); 318 319 if (parentNodePath.length() == 0) 320 { 321 return destSession.getRootNode(); 322 } 323 else if (destRootNode.hasNode(parentNodePath)) 324 { 325 // Found existing parent 326 return destRootNode.getNode(parentNodePath); 327 } 328 else 329 { 330 Node clonedAncestorNode = cloneAncestorsAndPreserveUUID(parentNode, destSession); 331 Node clonedParentNode = null; 332 333 if (clonedAncestorNode.hasNode(parentNode.getName())) 334 { 335 // Possible with autocreated children 336 clonedParentNode = clonedAncestorNode.getNode(parentNode.getName()); 337 } 338 else 339 { 340 clonedParentNode = addNodeWithUUID(parentNode, clonedAncestorNode, parentNode.getName()); 341 } 342 343 // Copy only properties 344 cloneNodeAndPreserveUUID(parentNode, clonedParentNode, PredicateUtils.truePredicate(), PredicateUtils.falsePredicate()); 345 346 return clonedParentNode; 347 } 348 } 349 } 350 351 /** 352 * Clone a content node, cloning its workflow along. 353 * @param destSession the destination session. 354 * @param node the node to clone. 355 * @return the cloned node. 356 * @throws RepositoryException if an error occurs. 357 */ 358 public Node cloneContentNodeWithWorkflow(Session destSession, Node node) throws RepositoryException 359 { 360 return cloneContentNodeWithWorkflow(destSession, node, PredicateUtils.truePredicate(), PredicateUtils.truePredicate(), null); 361 } 362 363 /** 364 * Clone a content node, cloning its workflow along. 365 * @param destSession the destination session. 366 * @param node the node to clone. 367 * @param propertyPredicate a test on the properties. 368 * @param nodePredicate a test on the nodes. 369 * @return the cloned node. 370 * @throws RepositoryException if an error occurs. 371 */ 372 public Node cloneContentNodeWithWorkflow(Session destSession, Node node, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 373 { 374 return cloneContentNodeWithWorkflow(destSession, node, propertyPredicate, nodePredicate, null); 375 } 376 377 /** 378 * Clone a content node, cloning its workflow along. 379 * @param destSession the destination session. 380 * @param node the node to clone. 381 * @param propertyPredicate a test on the properties. 382 * @param nodePredicate a test on the nodes. 383 * @param version the version of the node to clone. 384 * @return the cloned node. 385 * @throws RepositoryException if an error occurs. 386 */ 387 public Node cloneContentNodeWithWorkflow(Session destSession, Node node, Predicate propertyPredicate, Predicate nodePredicate, String version) throws RepositoryException 388 { 389 Node clonedContentParentNode = null; 390 391 String nodeName = node.getName(); 392 393 if (_inCollection(node)) 394 { 395 // Clone the ancestors until the collection (hashed nodes will be processed separatel). 396 Node destCollectionNode = cloneAncestorsAndPreserveUUID(node.getParent().getParent(), destSession); 397 // Search for an available node name. 398 nodeName = _getAvailableNodeName(destCollectionNode, node); 399 String[] path = _getHashedPath(nodeName); 400 401 // Create the two hashed nodes, the content will be created in the second one. 402 Node destHash1 = _getOrAddNode(destCollectionNode, path[0], AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE); 403 clonedContentParentNode = _getOrAddNode(destHash1, path[1], AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE); 404 } 405 else 406 { 407 clonedContentParentNode = cloneAncestorsAndPreserveUUID(node, destSession); 408 } 409 410 Node clonedNode = null; 411 try 412 { 413 clonedNode = destSession.getNodeByIdentifier(node.getIdentifier()); 414 } 415 catch (ItemNotFoundException e) 416 { 417 clonedNode = createNodeClone(node, clonedContentParentNode, nodeName); 418 } 419 420 cloneNodeAndPreserveUUID(node, clonedNode, propertyPredicate, nodePredicate); 421 422 // Clone unversioned metadata 423 String unversionedNodeName = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":unversioned"; 424 Node unversionedNode = null; 425 426 if (clonedNode.hasNode(unversionedNodeName)) 427 { 428 unversionedNode = clonedNode.getNode(unversionedNodeName); 429 } 430 else 431 { 432 unversionedNode = clonedNode.addNode(unversionedNodeName, "ametys:compositeMetadata"); 433 } 434 435 cloneNodeAndPreserveUUID(node.getNode(unversionedNodeName), unversionedNode, propertyPredicate, nodePredicate); 436 437 return clonedNode; 438 } 439 440 private boolean _inCollection(Node node) throws RepositoryException 441 { 442 boolean inCollection = false; 443 444 try 445 { 446 inCollection = node.getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE) 447 && node.getParent().getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE) 448 && node.getParent().getParent().getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_NODETYPE); 449 } 450 catch (ItemNotFoundException e) 451 { 452 // Ignore, one parent does not exist, just return false. 453 } 454 455 return inCollection; 456 } 457 458 private String _getAvailableNodeName(Node destCollectionNode, Node srcNode) throws RepositoryException 459 { 460 String baseName = srcNode.getName(); 461 String nodeName = NameHelper.filterName(baseName); 462 463 int index = 2; 464 while (_nodeExistsInCollection(destCollectionNode, nodeName)) 465 { 466 baseName = srcNode.getName() + "-" + index; 467 nodeName = NameHelper.filterName(baseName); 468 index++; 469 } 470 471 return nodeName; 472 } 473 474 private boolean _nodeExistsInCollection(Node collectionNode, String nodeName) throws RepositoryException 475 { 476 try 477 { 478 String[] path = _getHashedPath(nodeName); 479 return collectionNode.getNode(path[0]).getNode(path[1]).hasNode(nodeName); 480 } 481 catch (PathNotFoundException e) 482 { 483 // If a path element is not found, the node doesn't exist. 484 return false; 485 } 486 } 487 488 private String[] _getHashedPath(String name) 489 { 490 long hash = Math.abs(HashUtil.hash(name)); 491 String hashStr = Long.toString(hash, 16); 492 hashStr = StringUtils.leftPad(hashStr, 4, '0'); 493 494 return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)}; 495 } 496 497 private static Node _getOrAddNode(Node parent, String name, String type) throws RepositoryException 498 { 499 if (parent.hasNode(name)) 500 { 501 return parent.getNode(name); 502 } 503 else 504 { 505 return parent.addNode(name, type); 506 } 507 } 508 509 /** 510 * Create a clone of the specified node. 511 * @param srcNode the node to clone. 512 * @param parentNode the node under which to create the clonde. 513 * @param desiredNodeName the wanted node name. 514 * @return the cloned node. 515 * @throws RepositoryException if an error occurs. 516 */ 517 protected Node createNodeClone(Node srcNode, Node parentNode, String desiredNodeName) throws RepositoryException 518 { 519 Node clonedNode = null; 520 521 String nodeName = NameHelper.filterName(desiredNodeName); 522 523 int errorCount = 0; 524 do 525 { 526 try 527 { 528 clonedNode = addNodeWithUUID(srcNode, parentNode, nodeName); 529 } 530 catch (ItemExistsException e) 531 { 532 // Node name is already used. 533 errorCount++; 534 535 nodeName = NameHelper.filterName(desiredNodeName + " " + (errorCount + 1)); 536 } 537 } 538 while (clonedNode == null); 539 540 return clonedNode; 541 } 542 543 /** 544 * Reorder a node, mirroring the order in the default workspace. 545 * @param parentNode the parent of the source Node in the default workspace. 546 * @param nodeName the node name. 547 * @param node the node in the destination workspace to be reordered. 548 * @throws RepositoryException if an error occurs. 549 */ 550 public void orderNode(Node parentNode, String nodeName, Node node) throws RepositoryException 551 { 552 Session destSession = node.getSession(); 553 554 // iterate over the siblings to find the following 555 NodeIterator siblings = parentNode.getNodes(); 556 boolean iterate = true; 557 558 while (siblings.hasNext() && iterate) 559 { 560 Node sibling = siblings.nextNode(); 561 iterate = !sibling.getName().equals(nodeName); 562 } 563 564 // iterator is currently on the pageNode 565 566 Node nextSibling = null; 567 while (siblings.hasNext() && nextSibling == null) 568 { 569 Node sibling = siblings.nextNode(); 570 String name = sibling.getName(); 571 String path = sibling.getPath(); 572 if (!name.startsWith(RepositoryConstants.NAMESPACE_PREFIX + ":") && !name.startsWith(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":") && destSession.itemExists(path)) 573 { 574 nextSibling = sibling; 575 } 576 } 577 578 // nextSibling is either null meaning that the Node must be ordered last or is equals to the following sibling 579 if (nextSibling != null) 580 { 581 node.getParent().orderBefore(nodeName, nextSibling.getName()); 582 } 583 else 584 { 585 node.getParent().orderBefore(nodeName, null); 586 } 587 } 588 589}