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.data.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 if (getLogger().isDebugEnabled()) 240 { 241 getLogger().debug("Resolving " + absolutePath); 242 } 243 244 if (absolutePath == null) 245 { 246 throw new AmetysRepositoryException("Absolute path cannot be null"); 247 } 248 249 Node rootNode; 250 Session session = null; 251 try 252 { 253 session = _repository.login(); 254 rootNode = session.getRootNode().getNode(ROOT_REPO); 255 } 256 catch (PathNotFoundException e) 257 { 258 if (session != null) 259 { 260 session.logout(); 261 } 262 263 throw new AmetysRepositoryException("Unable to get ametys:root Node", e); 264 } 265 catch (RepositoryException e) 266 { 267 if (session != null) 268 { 269 session.logout(); 270 } 271 272 throw new AmetysRepositoryException("An error occured while getting ametys:root node", e); 273 } 274 275 try 276 { 277 return this.<A>_resolve(null, rootNode, absolutePath, false); 278 } 279 catch (RepositoryException e) 280 { 281 session.logout(); 282 283 throw new AmetysRepositoryException("An error occured while resolving " + absolutePath, e); 284 } 285 } 286 287 /** 288 * Retrieves an {@link AmetysObject} by its unique id. 289 * @param <A> the actual type of {@link AmetysObject}. 290 * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository. 291 * @return the corresponding {@link AmetysObject}. 292 * @throws AmetysRepositoryException if an error occurs. 293 * @throws UnknownAmetysObjectException if no such object exists for the given id. 294 */ 295 public <A extends AmetysObject> A resolveById(String id) throws AmetysRepositoryException, UnknownAmetysObjectException 296 { 297 if (getLogger().isDebugEnabled()) 298 { 299 getLogger().debug("Resolving " + id); 300 } 301 302 int index = id.indexOf("://"); 303 if (index == -1) 304 { 305 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 306 } 307 308 String scheme = id.substring(0, index); 309 310 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 311 312 if (factory == null) 313 { 314 throw new UnknownAmetysObjectException("There's no object for id " + id); 315 } 316 317 return factory.getAmetysObjectById(id); 318 } 319 320 /** 321 * <b>Expert</b>. Retrieves an {@link AmetysObject} by its unique id and the provided JCR Session.<br> 322 * It only works with id corresponding to a {@link JCRAmetysObjectFactory}.<br> 323 * This method should be uses to avoid useless Session creation. 324 * @param <A> the actual type of {@link AmetysObject}. 325 * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository. 326 * @param session the JCR Session to use to retrieve the {@link AmetysObject}. 327 * @return the corresponding {@link AmetysObject}. 328 * @throws AmetysRepositoryException if an error occurs. 329 * @throws UnknownAmetysObjectException if no such object exists for the given id. 330 * @throws RepositoryException if a JCR error occurs. 331 */ 332 public <A extends AmetysObject> A resolveById(String id, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 333 { 334 if (getLogger().isDebugEnabled()) 335 { 336 getLogger().debug("Resolving " + id); 337 } 338 339 int index = id.indexOf("://"); 340 if (index == -1) 341 { 342 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 343 } 344 345 String scheme = id.substring(0, index); 346 347 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 348 349 if (factory == null) 350 { 351 throw new UnknownAmetysObjectException("There's no object for id " + id); 352 } 353 354 if (!(factory instanceof JCRAmetysObjectFactory)) 355 { 356 throw new IllegalArgumentException("The expert method resolveById(String, Session) should only be called for id corresponding to a JCRAmetysObjectFactory"); 357 } 358 359 return ((JCRAmetysObjectFactory<A>) factory).getAmetysObjectById(id, session); 360 } 361 362 /** 363 * Return true if the specified id correspond to an existing {@link AmetysObject}. 364 * @param id the identifier. 365 * @return true if the specified id correspond to an existing {@link AmetysObject}. 366 * @throws AmetysRepositoryException if an error occurs. 367 */ 368 public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException 369 { 370 int index = id.indexOf("://"); 371 if (index == -1) 372 { 373 throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id); 374 } 375 376 String scheme = id.substring(0, index); 377 378 AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme); 379 380 if (factory == null) 381 { 382 return false; 383 } 384 385 return factory.hasAmetysObjectForId(id); 386 } 387 388 /** 389 * <b>Expert</b>. Returns the {@link AmetysObject} corresponding to a given JCR Node.<br> 390 * It is strictly equivalent to call <code>resolve(null, node, null, allowUnknownNode)</code> 391 * @param <A> the actual type of {@link AmetysObject}s 392 * @param node an existing node in the underlying JCR repository. 393 * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type 394 * does not correspond to a factory. If <code>false</code> and no factory 395 * corresponds, an {@link AmetysRepositoryException} is thrown. 396 * @return the {@link AmetysObject} corresponding to a given JCR node. 397 * @throws AmetysRepositoryException if an error occurs. 398 * @throws RepositoryException if a JCR error occurs. 399 */ 400 public <A extends AmetysObject> A resolve(Node node, boolean allowUnknownNode) throws AmetysRepositoryException, RepositoryException 401 { 402 return this.<A>_resolve(null, node, null, allowUnknownNode); 403 } 404 405 /** 406 * <b>Expert</b>. Retrieves an {@link AmetysObject}, given a JCR Node, a relative path 407 * and the parentPath in the Ametys hierarchy.<br> 408 * The path is always relative, even if it begins with a <code>'/'</code>, 409 * <code>null</code> path or empty path are equivalent.<br> 410 * May return null if ignoreUnknownNodes is true. 411 * @param <A> the actual type of {@link AmetysObject}. 412 * @param parentPath the parentPath of the returned AmetysObject, in the Ametys hierarchy. 413 * @param node the context JCR node. 414 * @param childPath the path relative to the JCR node. 415 * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type 416 * does not correspond to a factory. If <code>false</code> and no factory 417 * corresponds, an {@link AmetysRepositoryException} is thrown. 418 * @return the corresponding AmetysObject. 419 * @throws AmetysRepositoryException if an error occurs. 420 * @throws UnknownAmetysObjectException if no such object exists for the given path. 421 * @throws RepositoryException if a JCR error occurs. 422 */ 423 public <A extends AmetysObject> A resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 424 { 425 return this.<A>_resolve(parentPath, node, childPath, allowUnknownNode); 426 } 427 428 @SuppressWarnings("unchecked") 429 private <T extends AmetysObject> T _resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException 430 { 431 if (getLogger().isDebugEnabled()) 432 { 433 getLogger().debug("Entering _resolve with parentPath=" + parentPath + ", node=" + node.getPath() + ", childPath=" + childPath + ", ignoreUnknownNodes=" + allowUnknownNode); 434 } 435 436 String path = childPath == null ? "" : childPath; 437 path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1); 438 439 if (path.length() != 0 && (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1)))) 440 { 441 throw new AmetysRepositoryException("Path cannot begin or end with a space character"); 442 } 443 444 String nodeType = NodeTypeHelper.getNodeTypeName(node); 445 446 JCRAmetysObjectFactory jcrFactory = _getJCRFactory(nodeType, allowUnknownNode, parentPath, childPath); 447 448 if (jcrFactory == null) 449 { 450 return null; 451 } 452 453 AmetysObject rootObject = jcrFactory.getAmetysObject(node, parentPath); 454 455 if (path.length() != 0) 456 { 457 if (!(rootObject instanceof TraversableAmetysObject)) 458 { 459 throw new AmetysRepositoryException("The node of type '" + nodeType + "' at path '" + node.getPath() + "' does not corresponds to a TraversableAmetysObject"); 460 } 461 462 return (T) ((TraversableAmetysObject) rootObject).getChild(path); 463 } 464 else 465 { 466 return (T) rootObject; 467 } 468 } 469 470 471 private JCRAmetysObjectFactory _getJCRFactory(String nodeType, boolean allowUnknownNode, String parentPath, String childPath) 472 { 473 if (getLogger().isDebugEnabled()) 474 { 475 getLogger().debug("Nodetype is " + nodeType); 476 } 477 478 AmetysObjectFactory<?> factory = _ametysFactoryExtensionPoint.getFactoryForNodetype(nodeType); 479 480 if (factory == null) 481 { 482 if (allowUnknownNode) 483 { 484 if (getLogger().isDebugEnabled()) 485 { 486 getLogger().debug("No factory for nodetype " + nodeType + ". Unknown node is allowed, returning null."); 487 } 488 489 return null; 490 } 491 492 throw new UnknownAmetysObjectException("Cannot get factory for node '" + childPath + "' under '" + parentPath + "': There's no factory for nodetype: " + nodeType); 493 } 494 495 if (getLogger().isDebugEnabled()) 496 { 497 getLogger().debug("Factory is " + factory.getClass().getName()); 498 } 499 500 if (!(factory instanceof JCRAmetysObjectFactory)) 501 { 502 throw new AmetysRepositoryException("A factory resolving JCR nodes must implements JCRAmetysObjectFactory"); 503 } 504 505 JCRAmetysObjectFactory jcrFactory = (JCRAmetysObjectFactory) factory; 506 507 return jcrFactory; 508 } 509 510 /** 511 * <b>Expert</b>. Retrieves the virtual children of a concrete JCR Node.<br> 512 * @param <A> the actual type of {@link AmetysObject}s. 513 * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference. 514 * @return all virtual children under the given JCR Node in the Ametys hierarchy. 515 * @throws AmetysRepositoryException if an error occurs. 516 * @throws RepositoryException if a JCR error occurs. 517 */ 518 public <A extends AmetysObject> AmetysObjectIterable<A> resolveVirtualChildren(JCRAmetysObject parent) throws AmetysRepositoryException, RepositoryException 519 { 520 Node contextNode = parent.getNode(); 521 522 if (getLogger().isDebugEnabled()) 523 { 524 getLogger().debug("Entering resolveVirtualChildren with parent=" + parent); 525 } 526 527 if (!contextNode.hasProperty(VIRTUAL_PROPERTY)) 528 { 529 return null; 530 } 531 532 Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues(); 533 List<AmetysObjectIterable<A>> children = new ArrayList<>(values.length); 534 for (Value value : values) 535 { 536 String id = value.getString(); 537 538 if (getLogger().isDebugEnabled()) 539 { 540 getLogger().debug("Found virtual factory id: " + id); 541 } 542 543 AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getExtension(id); 544 545 if (factory == null) 546 { 547 throw new AmetysRepositoryException("There's no virtual factory for id " + id); 548 } 549 550 if (getLogger().isDebugEnabled()) 551 { 552 getLogger().debug("Found factory: " + factory.getClass().getName()); 553 } 554 555 if (!(factory instanceof VirtualAmetysObjectFactory)) 556 { 557 throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory"); 558 } 559 560 VirtualAmetysObjectFactory<A> virtualFactory = (VirtualAmetysObjectFactory<A>) factory; 561 children.add(virtualFactory.getChildren(parent)); 562 } 563 564 return new ChainedAmetysObjectIterable<>(children); 565 } 566 567 /** 568 * <b>Expert</b>. Retrieves the virtual child of a concrete JCR Node.<br> 569 * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference. 570 * @param childPath the name of the virtual child. 571 * @return a named child under the given JCR Node in the Ametys hierarchy. 572 * @throws AmetysRepositoryException if an error occurs. 573 * @throws RepositoryException if a JCR error occurs. 574 * @throws UnknownAmetysObjectException if the named child does not exist 575 */ 576 public AmetysObject resolveVirtualChild(JCRAmetysObject parent, String childPath) throws AmetysRepositoryException, RepositoryException, UnknownAmetysObjectException 577 { 578 Node contextNode = parent.getNode(); 579 580 if (getLogger().isDebugEnabled()) 581 { 582 getLogger().debug("Entering resolveVirtualChild with parent=" + parent); 583 } 584 585 if (!contextNode.hasProperty(VIRTUAL_PROPERTY)) 586 { 587 throw new UnknownAmetysObjectException("There's no virtual child at Ametys path " + parent.getPath()); 588 } 589 590 String path = childPath == null ? "" : childPath; 591 path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1); 592 int index = path.indexOf('/'); 593 String childName = index == -1 ? path : path.substring(0, index); 594 String subPath = index == -1 ? null : path.substring(index + 1); 595 596 if (childName.length() == 0) 597 { 598 throw new AmetysRepositoryException("A path element cannot be empty in " + childPath); 599 } 600 else if (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1))) 601 { 602 throw new AmetysRepositoryException("Path element cannot begin or end with a space character: " + childName); 603 } 604 605 Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues(); 606 AmetysObject object = _getVirtualChild(parent, childName, values); 607 608 if (object == null) 609 { 610 throw new UnknownAmetysObjectException("There's no virtual object named " + childName + " at Ametys path " + parent.getPath()); 611 } 612 613 if (subPath != null) 614 { 615 if (!(object instanceof TraversableAmetysObject)) 616 { 617 throw new AmetysRepositoryException("The virtual object " + childName + "at path '" + childPath + "' does not corresponds to a TraversableAmetysObject"); 618 } 619 620 return ((TraversableAmetysObject) object).getChild(subPath); 621 } 622 else 623 { 624 return object; 625 } 626 } 627 628 /** 629 * Executes the given JCR XPath query and resolves results as 630 * {@link AmetysObject}s.<br> 631 * The resulting {@link AmetysObjectIterable} supports lazy loading, but 632 * will also fail lazily if one if the result nodes does not correspond to 633 * an {@link AmetysObject}. 634 * @param <A> the actual type of the results. 635 * @param jcrQuery a JCR XPath query. 636 * @return an Iterator over the resulting {@link AmetysObject}. 637 */ 638 public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery) 639 { 640 Session session = null; 641 try 642 { 643 session = _repository.login(); 644 return query(jcrQuery, session); 645 } 646 catch (RepositoryException ex) 647 { 648 if (session != null) 649 { 650 session.logout(); 651 } 652 653 throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex); 654 } 655 } 656 657 /** 658 * <b>Expert</b>. Executes the given JCR XPath query with the provided JCR Session and resolves results as 659 * {@link AmetysObject}s.<br> 660 * The resulting {@link AmetysObjectIterable} supports lazy loading, but 661 * will also fail lazily if one if the result nodes does not correspond to 662 * an {@link AmetysObject}. 663 * @param <A> the actual type of the results. 664 * @param jcrQuery a JCR XPath query. 665 * @param session the JCR Session to use to execute the request. 666 * @return an Iterator over the resulting {@link AmetysObject}. 667 * @throws RepositoryException if a JCR error occurs. 668 */ 669 @SuppressWarnings("deprecation") 670 public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery, Session session) throws RepositoryException 671 { 672 if (getLogger().isDebugEnabled()) 673 { 674 getLogger().debug("Executing XPath query: '" + jcrQuery + "'"); 675 } 676 677 Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH); 678 679 long t1 = System.currentTimeMillis(); 680 AmetysObjectIterable<A> it = new NodeIteratorIterable<>(this, query.execute().getNodes(), null, session); 681 682 if (getLogger().isInfoEnabled()) 683 { 684 getLogger().info("JCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms"); 685 } 686 687 return it; 688 } 689 690 private AmetysObject _getVirtualChild(JCRAmetysObject parent, String childName, Value[] values) throws RepositoryException 691 { 692 int i = 0; 693 AmetysObject object = null; 694 695 while (object == null && i < values.length) 696 { 697 Value value = values[i]; 698 String id = value.getString(); 699 700 if (getLogger().isDebugEnabled()) 701 { 702 getLogger().debug("Found virtual factory id: " + id); 703 } 704 705 AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getExtension(id); 706 707 if (factory == null) 708 { 709 throw new AmetysRepositoryException("There's no virtual factory for id " + id); 710 } 711 712 if (getLogger().isDebugEnabled()) 713 { 714 getLogger().debug("Found factory: " + factory.getClass().getName()); 715 } 716 717 if (!(factory instanceof VirtualAmetysObjectFactory)) 718 { 719 throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id); 720 } 721 722 VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory; 723 724 try 725 { 726 object = virtualFactory.getChild(parent, childName); 727 } 728 catch (UnknownAmetysObjectException e) 729 { 730 // Not an error 731 if (getLogger().isDebugEnabled()) 732 { 733 getLogger().debug("The factory: " + factory.getClass().getName() + " has no child named" + childName, e); 734 } 735 736 i++; 737 } 738 } 739 740 return object; 741 } 742 743 /** 744 * <b>Expert</b>. Creates a child object in the JCR tree and resolve it to an {@link AmetysObject}. 745 * @param <A> the actual type of {@link AmetysObject}s 746 * @param parentPath the parentPath of the new object. 747 * @param parentNode the parent JCR Node of the new object. 748 * @param childName the name of the new object. 749 * @param nodetype the type of the Node backing the new object. 750 * @return the newly created {@link AmetysObject}. 751 * @throws AmetysRepositoryException if an error occurs. 752 * @throws RepositoryIntegrityViolationException if an object with the same name already 753 * exists and same name siblings is not allowed. 754 * @throws RepositoryException if a JCR error occurs. 755 */ 756 public <A extends AmetysObject> A createAndResolve(String parentPath, Node parentNode, String childName, String nodetype) throws AmetysRepositoryException, RepositoryIntegrityViolationException, RepositoryException 757 { 758 if (getLogger().isDebugEnabled()) 759 { 760 getLogger().debug("Entering createAndResolve with parentPath=" + parentPath + ", parentNode=" + parentNode.getPath() + ", childName=" + childName + ", nodetype=" + nodetype); 761 } 762 763 if (_ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) == null) 764 { 765 throw new AmetysRepositoryException("Cannot create a node '" + childName + "' under '" + parentPath + "': There's no factory for nodetype: " + nodetype); 766 } 767 768 try 769 { 770 Node node = parentNode.addNode(childName, nodetype); 771 NodeType[] mixinNodeTypes = node.getMixinNodeTypes(); 772 boolean foundMixin = false; 773 774 int i = 0; 775 while (!foundMixin && i < mixinNodeTypes.length) 776 { 777 if (OBJECT_TYPE.equals(mixinNodeTypes[i].getName())) 778 { 779 foundMixin = true; 780 } 781 782 i++; 783 } 784 785 if (!foundMixin) 786 { 787 node.addMixin(OBJECT_TYPE); 788 } 789 790 return this.<A>resolve(parentPath, node, null, false); 791 } 792 catch (ItemExistsException e) 793 { 794 throw new RepositoryIntegrityViolationException("The object " + childName + " already exist at path " + parentPath, e); 795 } 796 catch (RepositoryException e) 797 { 798 throw new AmetysRepositoryException("Unable to add child node for the underlying node for object at path " + parentPath, e); 799 } 800 } 801}