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