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