001/* 002 * Copyright 2010 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 */ 016 017package org.ametys.plugins.repository; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.List; 025 026import javax.jcr.ItemExistsException; 027import javax.jcr.NamespaceRegistry; 028import javax.jcr.Node; 029import javax.jcr.PathNotFoundException; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033import javax.jcr.Value; 034import javax.jcr.nodetype.NodeType; 035import javax.jcr.query.Query; 036 037import org.apache.avalon.framework.activity.Initializable; 038import org.apache.avalon.framework.component.Component; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.excalibur.source.Source; 044import org.apache.excalibur.source.SourceResolver; 045import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; 046import org.apache.jackrabbit.core.nodetype.NodeTypeDefStore; 047import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; 048import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; 049import org.apache.jackrabbit.spi.Name; 050import org.apache.jackrabbit.spi.QNodeTypeDefinition; 051import org.slf4j.Logger; 052 053import org.ametys.core.util.LambdaUtils; 054import org.ametys.plugins.repository.jcr.JCRAmetysObject; 055import org.ametys.plugins.repository.jcr.JCRAmetysObjectFactory; 056import org.ametys.plugins.repository.jcr.NodeTypeHelper; 057import org.ametys.plugins.repository.migration.jcr.repository.VersionsFactory; 058import org.ametys.plugins.repository.provider.AbstractRepository; 059import org.ametys.plugins.repository.provider.JackrabbitRepository; 060import org.ametys.plugins.repository.virtual.VirtualAmetysObjectFactory; 061import org.ametys.runtime.plugin.component.AbstractLogEnabled; 062 063/** 064 * Base component for accessing {@link AmetysObject}s. 065 */ 066public class AmetysObjectResolver extends AbstractLogEnabled implements Serviceable, Initializable, Component 067{ 068 /** Avalon ROLE. */ 069 public static final String ROLE = AmetysObjectResolver.class.getName(); 070 071 /** JCR Relative Path to root. */ 072 public static final String ROOT_REPO = "ametys:root"; 073 074 /** JCR type for root node. */ 075 public static final String ROOT_TYPE = "ametys:root"; 076 077 /** JCR mixin type for objects. */ 078 public static final String OBJECT_TYPE = "ametys:object"; 079 080 /** JCR property name for virtual objects. */ 081 public static final String VIRTUAL_PROPERTY = "ametys-internal:virtual"; 082 083 private AmetysObjectFactoryExtensionPoint _ametysFactoryExtensionPoint; 084 private NamespacesExtensionPoint _namespacesExtensionPoint; 085 private NodeTypeDefinitionsExtensionPoint _nodetypeDefsExtensionPoint; 086 private Repository _repository; 087 private SourceResolver _resolver; 088 089 090 @Override 091 public void service(ServiceManager manager) throws ServiceException 092 { 093 _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 094 _repository = (Repository) manager.lookup(AbstractRepository.ROLE); 095 _ametysFactoryExtensionPoint = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 096 _namespacesExtensionPoint = (NamespacesExtensionPoint) manager.lookup(NamespacesExtensionPoint.ROLE); 097 _nodetypeDefsExtensionPoint = (NodeTypeDefinitionsExtensionPoint) manager.lookup(NodeTypeDefinitionsExtensionPoint.ROLE); 098 } 099 100 @Override 101 public void initialize() throws Exception 102 { 103 // On vérifie que le root soit bien créé 104 Session session = _repository.login(); 105 106 _initNamespaces(session); 107 _initNodetypes(session); 108 109 if (!session.getRootNode().hasNode(ROOT_REPO)) 110 { 111 getLogger().info("Creating ametys root Node"); 112 113 session.getRootNode().addNode(ROOT_REPO, ROOT_TYPE); 114 115 initRepoNodes(session, getLogger()); 116 } 117 118 if (session.hasPendingChanges()) 119 { 120 session.save(); 121 } 122 123 session.logout(); 124 if (_repository instanceof JackrabbitRepository) 125 { 126 // Now that custom_nodetypes.xml have been (re)created, compare it with the previous version and delete the backup if they are the same 127 ((JackrabbitRepository) _repository).compareCustomNodetypes(); 128 } 129 } 130 131 /** 132 * Init the repository nodes directly inside ROOT node 133 * @param session the session to use 134 * @param logger the logger to log actions 135 * @return true if the sesion needs changes 136 * @throws RepositoryException something went wrong 137 */ 138 public static boolean initRepoNodes(Session session, Logger logger) throws RepositoryException 139 { 140 // Create Ametys migration versions node if we just created the repository. 141 logger.info("Creating ametys migration versions root Node"); 142 session.getRootNode().getNode(ROOT_REPO) 143 .addNode(VersionsFactory.VERSIONS_NODENAME, VersionsFactory.VERSIONS_NODETYPE); 144 145 return session.hasPendingChanges(); 146 } 147 148 private void _initNamespaces(Session session) throws RepositoryException 149 { 150 NamespaceRegistry registry = session.getWorkspace().getNamespaceRegistry(); 151 Collection prefixes = Arrays.asList(registry.getPrefixes()); 152 153 _namespacesExtensionPoint.getExtensionsIds().stream() 154 .filter(prefix -> !prefixes.contains(prefix)) 155 .forEach(LambdaUtils.wrapConsumer(prefix -> 156 { 157 String namespace = _namespacesExtensionPoint.getNamespace(prefix); 158 getLogger().debug("Adding {} namespace", prefix); 159 registry.registerNamespace(prefix, namespace); 160 })); 161 } 162 163 private void _initNodetypes(Session session) throws RepositoryException, InvalidNodeTypeDefException, IOException 164 { 165 NodeTypeDefStore store = new NodeTypeDefStore(); 166 167 // Hard-coded first nodetypes file 168 Source fsource = _resolver.resolveURI("plugin:repository://nodetypes/ametys_nodetypes.xml"); 169 try (InputStream is = fsource.getInputStream()) 170 { 171 store.load(is); 172 } 173 finally 174 { 175 _resolver.release(fsource); 176 } 177 178 // Load all declared nodetype definitions in the store. 179 for (String nodetypeDef : _nodetypeDefsExtensionPoint.getNodeTypeDefinitions()) 180 { 181 Source source = _resolver.resolveURI(nodetypeDef); 182 try (InputStream is = source.getInputStream()) 183 { 184 store.load(is); 185 } 186 finally 187 { 188 _resolver.release(source); 189 } 190 } 191 192 NodeTypeManagerImpl ntManager = (NodeTypeManagerImpl) session.getWorkspace().getNodeTypeManager(); 193 NodeTypeRegistry registry = ntManager.getNodeTypeRegistry(); 194 195 // Remove all already registered nodetypes from the store. 196 for (Name name : registry.getRegisteredNodeTypes()) 197 { 198 store.remove(name); 199 } 200 201 // Register the "new" nodetype definitions. 202 Collection<QNodeTypeDefinition> ntDefs = store.all(); 203 if (!ntDefs.isEmpty()) 204 { 205 registry.registerNodeTypes(ntDefs); 206 } 207 } 208 209 /** 210 * Retrieves an {@link AmetysObject} from an absolute path. 211 * The given path is absolute in the Ametys tree.<br> 212 * The path may omit the leading <code>'/'</code>, but the path 213 * is always considered absolute, <code>null</code> path is forbidden.<br> 214 * @param <A> the actual type of {@link AmetysObject}. 215 * @param absolutePath the path to use. 216 * @return the corresponding AmetysObject. 217 * @throws AmetysRepositoryException if an error occurs. 218 * @throws UnknownAmetysObjectException if no such object exists for the given path. 219 * @deprecated Use resolveByPath instead 220 */ 221 @Deprecated 222 public <A extends AmetysObject> A resolve(String absolutePath) throws AmetysRepositoryException, UnknownAmetysObjectException 223 { 224 return resolveByPath(absolutePath); 225 } 226 227 /** 228 * Retrieves an {@link AmetysObject} from an absolute path. 229 * The given path is absolute in the Ametys tree.<br> 230 * The path may omit the leading <code>'/'</code>, but the path 231 * is always considered absolute, <code>null</code> path is forbidden.<br> 232 * @param <A> the actual type of {@link AmetysObject}. 233 * @param absolutePath the path to use. 234 * @return the corresponding AmetysObject. 235 * @throws AmetysRepositoryException if an error occurs. 236 * @throws UnknownAmetysObjectException if no such object exists for the given path. 237 */ 238 public <A extends AmetysObject> A resolveByPath(String absolutePath) throws AmetysRepositoryException, UnknownAmetysObjectException 239 { 240 return resolveByPath(absolutePath, null); 241 } 242 243 /** 244 * Retrieves an {@link AmetysObject} from an absolute path. 245 * The given path is absolute in the Ametys tree.<br> 246 * The path may omit the leading <code>'/'</code>, but the path 247 * is always considered absolute, <code>null</code> path is forbidden.<br> 248 * @param <A> the actual type of {@link AmetysObject}. 249 * @param absolutePath the path to use. 250 * @param session the JCR Session to use to retrieve the {@link AmetysObject}. 251 * @return the corresponding AmetysObject. 252 * @throws AmetysRepositoryException if an error occurs. 253 * @throws UnknownAmetysObjectException if no such object exists for the given path. 254 */ 255 public <A extends AmetysObject> A resolveByPath(String absolutePath, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException 256 { 257 if (getLogger().isDebugEnabled()) 258 { 259 getLogger().debug("Resolving " + absolutePath); 260 } 261 262 if (absolutePath == null) 263 { 264 throw new AmetysRepositoryException("Absolute path cannot be null"); 265 } 266 267 Node rootNode; 268 Session jcrSession = null; 269 try 270 { 271 jcrSession = session != null ? session : _repository.login(); 272 rootNode = jcrSession.getRootNode().getNode(ROOT_REPO); 273 } 274 catch (PathNotFoundException e) 275 { 276 if (session == null && jcrSession != null) 277 { 278 // logout only if the session was created here 279 jcrSession.logout(); 280 } 281 282 throw new AmetysRepositoryException("Unable to get ametys:root Node", e); 283 } 284 catch (RepositoryException e) 285 { 286 if (session == null && jcrSession != null) 287 { 288 // logout only if the session was created here 289 jcrSession.logout(); 290 } 291 292 throw new AmetysRepositoryException("An error occured while getting ametys:root node", e); 293 } 294 295 try 296 { 297 return this.<A>_resolve(null, rootNode, absolutePath, false); 298 } 299 catch (RepositoryException e) 300 { 301 if (session == null) 302 { 303 // logout only if the session was created here 304 jcrSession.logout(); 305 } 306 307 throw new AmetysRepositoryException("An error occured while resolving " + absolutePath, e); 308 } 309 } 310 311 /** 312 * Retrieves an {@link AmetysObject} by its unique id. 313 * @param <A> the actual type of {@link AmetysObject}. 314 * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository. 315 * @return the corresponding {@link AmetysObject}. 316 * @throws AmetysRepositoryException if an error occurs. 317 * @throws UnknownAmetysObjectException if no such object exists for the given id. 318 */ 319 public <A extends AmetysObject> A resolveById(String id) throws AmetysRepositoryException, UnknownAmetysObjectException 320 { 321 if (getLogger().isDebugEnabled()) 322 { 323 getLogger().debug("Resolving " + id); 324 } 325 326 if (StringUtils.isBlank(id)) 327 { 328 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax but id is blank or null"); 329 } 330 331 int index = id.indexOf("://"); 332 if (index == -1) 333 { 334 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 335 } 336 337 String scheme = id.substring(0, index); 338 339 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 340 341 if (factory == null) 342 { 343 throw new UnknownAmetysObjectException("There's no object for id " + id); 344 } 345 346 return factory.getAmetysObjectById(id); 347 } 348 349 /** 350 * <b>Expert</b>. Retrieves an {@link AmetysObject} by its unique id and the provided JCR Session.<br> 351 * It only works with id corresponding to a {@link JCRAmetysObjectFactory}.<br> 352 * This method should be uses to avoid useless Session creation. 353 * @param <A> the actual type of {@link AmetysObject}. 354 * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository. 355 * @param session the JCR Session to use to retrieve the {@link AmetysObject}. 356 * @return the corresponding {@link AmetysObject}. 357 * @throws AmetysRepositoryException if an error occurs. 358 * @throws UnknownAmetysObjectException if no such object exists for the given id. 359 * @throws RepositoryException if a JCR error occurs. 360 */ 361 public <A extends AmetysObject> A resolveById(String id, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 362 { 363 if (getLogger().isDebugEnabled()) 364 { 365 getLogger().debug("Resolving " + id); 366 } 367 368 if (StringUtils.isBlank(id)) 369 { 370 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax but id is blank or null"); 371 } 372 373 int index = id.indexOf("://"); 374 if (index == -1) 375 { 376 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 377 } 378 379 String scheme = id.substring(0, index); 380 381 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 382 383 if (factory == null) 384 { 385 throw new UnknownAmetysObjectException("There's no object for id " + id); 386 } 387 388 if (!(factory instanceof JCRAmetysObjectFactory)) 389 { 390 throw new IllegalArgumentException("The expert method resolveById(String, Session) should only be called for id corresponding to a JCRAmetysObjectFactory"); 391 } 392 393 return ((JCRAmetysObjectFactory<A>) factory).getAmetysObjectById(id, session); 394 } 395 396 /** 397 * Return true if the specified id correspond to an existing {@link AmetysObject}. 398 * @param id the identifier. 399 * @return true if the specified id correspond to an existing {@link AmetysObject}. 400 * @throws AmetysRepositoryException if an error occurs. 401 */ 402 public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException 403 { 404 int index = id.indexOf("://"); 405 if (index == -1) 406 { 407 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 408 } 409 410 String scheme = id.substring(0, index); 411 412 AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 413 414 if (factory == null) 415 { 416 return false; 417 } 418 419 return factory.hasAmetysObjectForId(id); 420 } 421 422 /** 423 * <b>Expert</b>. Returns the {@link AmetysObject} corresponding to a given JCR Node.<br> 424 * It is strictly equivalent to call <code>resolve(null, node, null, allowUnknownNode)</code> 425 * @param <A> the actual type of {@link AmetysObject}s 426 * @param node an existing node in the underlying JCR repository. 427 * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type 428 * does not correspond to a factory. If <code>false</code> and no factory 429 * corresponds, an {@link AmetysRepositoryException} is thrown. 430 * @return the {@link AmetysObject} corresponding to a given JCR node. 431 * @throws AmetysRepositoryException if an error occurs. 432 * @throws RepositoryException if a JCR error occurs. 433 */ 434 public <A extends AmetysObject> A resolve(Node node, boolean allowUnknownNode) throws AmetysRepositoryException, RepositoryException 435 { 436 return this.<A>_resolve(null, node, null, allowUnknownNode); 437 } 438 439 /** 440 * <b>Expert</b>. Retrieves an {@link AmetysObject}, given a JCR Node, a relative path 441 * and the parentPath in the Ametys hierarchy.<br> 442 * The path is always relative, even if it begins with a <code>'/'</code>, 443 * <code>null</code> path or empty path are equivalent.<br> 444 * May return null if ignoreUnknownNodes is true. 445 * @param <A> the actual type of {@link AmetysObject}. 446 * @param parentPath the parentPath of the returned AmetysObject, in the Ametys hierarchy. 447 * @param node the context JCR node. 448 * @param childPath the path relative to the JCR node. 449 * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type 450 * does not correspond to a factory. If <code>false</code> and no factory 451 * corresponds, an {@link AmetysRepositoryException} is thrown. 452 * @return the corresponding AmetysObject. 453 * @throws AmetysRepositoryException if an error occurs. 454 * @throws UnknownAmetysObjectException if no such object exists for the given path. 455 * @throws RepositoryException if a JCR error occurs. 456 */ 457 public <A extends AmetysObject> A resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 458 { 459 return this.<A>_resolve(parentPath, node, childPath, allowUnknownNode); 460 } 461 462 @SuppressWarnings("unchecked") 463 private <T extends AmetysObject> T _resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 464 { 465 if (getLogger().isDebugEnabled()) 466 { 467 getLogger().debug("Entering _resolve with parentPath=" + parentPath + ", node=" + node.getPath() + ", childPath=" + childPath + ", ignoreUnknownNodes=" + allowUnknownNode); 468 } 469 470 String path = childPath == null ? "" : childPath; 471 path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1); 472 473 if (path.length() != 0 && (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1)))) 474 { 475 throw new AmetysRepositoryException("Path cannot begin or end with a space character"); 476 } 477 478 String nodeType = NodeTypeHelper.getNodeTypeName(node); 479 480 JCRAmetysObjectFactory jcrFactory = _getJCRFactory(nodeType, allowUnknownNode, parentPath, childPath); 481 482 if (jcrFactory == null) 483 { 484 return null; 485 } 486 487 AmetysObject rootObject = jcrFactory.getAmetysObject(node, parentPath); 488 489 if (path.length() != 0) 490 { 491 if (!(rootObject instanceof TraversableAmetysObject)) 492 { 493 throw new AmetysRepositoryException("The node of type '" + nodeType + "' at path '" + node.getPath() + "' does not corresponds to a TraversableAmetysObject"); 494 } 495 496 return (T) ((TraversableAmetysObject) rootObject).getChild(path); 497 } 498 else 499 { 500 return (T) rootObject; 501 } 502 } 503 504 505 private JCRAmetysObjectFactory _getJCRFactory(String nodeType, boolean allowUnknownNode, String parentPath, String childPath) 506 { 507 if (getLogger().isDebugEnabled()) 508 { 509 getLogger().debug("Nodetype is " + nodeType); 510 } 511 512 AmetysObjectFactory<?> factory = _ametysFactoryExtensionPoint.getFactoryForNodetype(nodeType); 513 514 if (factory == null) 515 { 516 if (allowUnknownNode) 517 { 518 if (getLogger().isDebugEnabled()) 519 { 520 getLogger().debug("No factory for nodetype " + nodeType + ". Unknown node is allowed, returning null."); 521 } 522 523 return null; 524 } 525 526 throw new UnknownAmetysObjectException("Cannot get factory for node '" + childPath + "' under '" + parentPath + "': There's no factory for nodetype: " + nodeType); 527 } 528 529 if (getLogger().isDebugEnabled()) 530 { 531 getLogger().debug("Factory is " + factory.getClass().getName()); 532 } 533 534 if (!(factory instanceof JCRAmetysObjectFactory)) 535 { 536 throw new AmetysRepositoryException("A factory resolving JCR nodes must implements JCRAmetysObjectFactory"); 537 } 538 539 JCRAmetysObjectFactory jcrFactory = (JCRAmetysObjectFactory) factory; 540 541 return jcrFactory; 542 } 543 544 /** 545 * <b>Expert</b>. Retrieves the virtual children of a concrete JCR Node.<br> 546 * @param <A> the actual type of {@link AmetysObject}s. 547 * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference. 548 * @return all virtual children under the given JCR Node in the Ametys hierarchy. 549 * @throws AmetysRepositoryException if an error occurs. 550 * @throws RepositoryException if a JCR error occurs. 551 */ 552 public <A extends AmetysObject> AmetysObjectIterable<A> resolveVirtualChildren(JCRAmetysObject parent) throws AmetysRepositoryException, RepositoryException 553 { 554 Node contextNode = parent.getNode(); 555 556 if (getLogger().isDebugEnabled()) 557 { 558 getLogger().debug("Entering resolveVirtualChildren with parent=" + parent); 559 } 560 561 if (!contextNode.hasProperty(VIRTUAL_PROPERTY)) 562 { 563 return null; 564 } 565 566 Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues(); 567 List<AmetysObjectIterable<A>> children = new ArrayList<>(values.length); 568 for (Value value : values) 569 { 570 String id = value.getString(); 571 572 if (getLogger().isDebugEnabled()) 573 { 574 getLogger().debug("Found virtual factory id: " + id); 575 } 576 577 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getExtension(id); 578 579 if (factory == null) 580 { 581 throw new AmetysRepositoryException("There's no virtual factory for id " + id); 582 } 583 584 if (getLogger().isDebugEnabled()) 585 { 586 getLogger().debug("Found factory: " + factory.getClass().getName()); 587 } 588 589 if (!(factory instanceof VirtualAmetysObjectFactory)) 590 { 591 throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory"); 592 } 593 594 VirtualAmetysObjectFactory<A> virtualFactory = (VirtualAmetysObjectFactory<A>) factory; 595 children.add(virtualFactory.getChildren(parent)); 596 } 597 598 return new ChainedAmetysObjectIterable<>(children); 599 } 600 601 /** 602 * <b>Expert</b>. Retrieves the virtual child of a concrete JCR Node.<br> 603 * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference. 604 * @param childPath the name of the virtual child. 605 * @return a named child under the given JCR Node in the Ametys hierarchy. 606 * @throws AmetysRepositoryException if an error occurs. 607 * @throws RepositoryException if a JCR error occurs. 608 * @throws UnknownAmetysObjectException if the named child does not exist 609 */ 610 public AmetysObject resolveVirtualChild(JCRAmetysObject parent, String childPath) throws AmetysRepositoryException, RepositoryException, UnknownAmetysObjectException 611 { 612 Node contextNode = parent.getNode(); 613 614 if (getLogger().isDebugEnabled()) 615 { 616 getLogger().debug("Entering resolveVirtualChild with parent=" + parent); 617 } 618 619 if (!contextNode.hasProperty(VIRTUAL_PROPERTY)) 620 { 621 throw new UnknownAmetysObjectException("There's no virtual child at Ametys path " + parent.getPath()); 622 } 623 624 String path = childPath == null ? "" : childPath; 625 path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1); 626 int index = path.indexOf('/'); 627 String childName = index == -1 ? path : path.substring(0, index); 628 String subPath = index == -1 ? null : path.substring(index + 1); 629 630 if (childName.length() == 0) 631 { 632 throw new AmetysRepositoryException("A path element cannot be empty in " + childPath); 633 } 634 else if (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1))) 635 { 636 throw new AmetysRepositoryException("Path element cannot begin or end with a space character: " + childName); 637 } 638 639 Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues(); 640 AmetysObject object = _getVirtualChild(parent, childName, values); 641 642 if (object == null) 643 { 644 throw new UnknownAmetysObjectException("There's no virtual object named " + childName + " at Ametys path " + parent.getPath()); 645 } 646 647 if (subPath != null) 648 { 649 if (!(object instanceof TraversableAmetysObject)) 650 { 651 throw new AmetysRepositoryException("The virtual object " + childName + "at path '" + childPath + "' does not corresponds to a TraversableAmetysObject"); 652 } 653 654 return ((TraversableAmetysObject) object).getChild(subPath); 655 } 656 else 657 { 658 return object; 659 } 660 } 661 662 /** 663 * Executes the given JCR XPath query and resolves results as 664 * {@link AmetysObject}s.<br> 665 * The resulting {@link AmetysObjectIterable} supports lazy loading, but 666 * will also fail lazily if one if the result nodes does not correspond to 667 * an {@link AmetysObject}. 668 * @param <A> the actual type of the results. 669 * @param jcrQuery a JCR XPath query. 670 * @return an Iterator over the resulting {@link AmetysObject}. 671 */ 672 public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery) 673 { 674 Session session = null; 675 try 676 { 677 session = _repository.login(); 678 return query(jcrQuery, session); 679 } 680 catch (RepositoryException ex) 681 { 682 if (session != null) 683 { 684 session.logout(); 685 } 686 687 throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex); 688 } 689 } 690 691 /** 692 * <b>Expert</b>. Executes the given JCR XPath query with the provided JCR Session and resolves results as 693 * {@link AmetysObject}s.<br> 694 * The resulting {@link AmetysObjectIterable} supports lazy loading, but 695 * will also fail lazily if one if the result nodes does not correspond to 696 * an {@link AmetysObject}. 697 * @param <A> the actual type of the results. 698 * @param jcrQuery a JCR XPath query. 699 * @param session the JCR Session to use to execute the request. 700 * @return an Iterator over the resulting {@link AmetysObject}. 701 * @throws RepositoryException if a JCR error occurs. 702 */ 703 @SuppressWarnings("deprecation") 704 public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery, Session session) throws RepositoryException 705 { 706 if (getLogger().isDebugEnabled()) 707 { 708 getLogger().debug("Executing XPath query: '" + jcrQuery + "'"); 709 } 710 711 Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH); 712 713 long t1 = System.currentTimeMillis(); 714 AmetysObjectIterable<A> it = new NodeIteratorIterable<>(this, query.execute().getNodes(), null, session); 715 716 if (getLogger().isInfoEnabled()) 717 { 718 getLogger().info("JCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms"); 719 } 720 721 return it; 722 } 723 724 private AmetysObject _getVirtualChild(JCRAmetysObject parent, String childName, Value[] values) throws RepositoryException 725 { 726 int i = 0; 727 AmetysObject object = null; 728 729 while (object == null && i < values.length) 730 { 731 Value value = values[i]; 732 String id = value.getString(); 733 734 if (getLogger().isDebugEnabled()) 735 { 736 getLogger().debug("Found virtual factory id: " + id); 737 } 738 739 AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getExtension(id); 740 741 if (factory == null) 742 { 743 throw new AmetysRepositoryException("There's no virtual factory for id " + id); 744 } 745 746 if (getLogger().isDebugEnabled()) 747 { 748 getLogger().debug("Found factory: " + factory.getClass().getName()); 749 } 750 751 if (!(factory instanceof VirtualAmetysObjectFactory)) 752 { 753 throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id); 754 } 755 756 VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory; 757 758 try 759 { 760 object = virtualFactory.getChild(parent, childName); 761 } 762 catch (UnknownAmetysObjectException e) 763 { 764 // Not an error 765 if (getLogger().isDebugEnabled()) 766 { 767 getLogger().debug("The factory: " + factory.getClass().getName() + " has no child named" + childName, e); 768 } 769 770 i++; 771 } 772 } 773 774 return object; 775 } 776 777 /** 778 * <b>Expert</b>. Creates a child object in the JCR tree and resolve it to an {@link AmetysObject}. 779 * @param <A> the actual type of {@link AmetysObject}s 780 * @param parentPath the parentPath of the new object. 781 * @param parentNode the parent JCR Node of the new object. 782 * @param childName the name of the new object. 783 * @param nodetype the type of the Node backing the new object. 784 * @return the newly created {@link AmetysObject}. 785 * @throws AmetysRepositoryException if an error occurs. 786 * @throws RepositoryIntegrityViolationException if an object with the same name already 787 * exists and same name siblings is not allowed. 788 * @throws RepositoryException if a JCR error occurs. 789 */ 790 public <A extends AmetysObject> A createAndResolve(String parentPath, Node parentNode, String childName, String nodetype) throws AmetysRepositoryException, RepositoryIntegrityViolationException, RepositoryException 791 { 792 if (getLogger().isDebugEnabled()) 793 { 794 getLogger().debug("Entering createAndResolve with parentPath=" + parentPath + ", parentNode=" + parentNode.getPath() + ", childName=" + childName + ", nodetype=" + nodetype); 795 } 796 797 if (_ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) == null) 798 { 799 throw new AmetysRepositoryException("Cannot create a node '" + childName + "' under '" + parentPath + "': There's no factory for nodetype: " + nodetype); 800 } 801 802 try 803 { 804 Node node = parentNode.addNode(childName, nodetype); 805 NodeType[] mixinNodeTypes = node.getMixinNodeTypes(); 806 boolean foundMixin = false; 807 808 int i = 0; 809 while (!foundMixin && i < mixinNodeTypes.length) 810 { 811 if (OBJECT_TYPE.equals(mixinNodeTypes[i].getName())) 812 { 813 foundMixin = true; 814 } 815 816 i++; 817 } 818 819 if (!foundMixin) 820 { 821 node.addMixin(OBJECT_TYPE); 822 } 823 824 return this.<A>resolve(parentPath, node, null, false); 825 } 826 catch (ItemExistsException e) 827 { 828 throw new RepositoryIntegrityViolationException("The object " + childName + " already exist at path " + parentPath, e); 829 } 830 catch (RepositoryException e) 831 { 832 throw new AmetysRepositoryException("Unable to add child node for the underlying node for object at path " + parentPath, e); 833 } 834 } 835}