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.jcr; 018 019import java.util.ArrayList; 020import java.util.List; 021 022import javax.jcr.ItemExistsException; 023import javax.jcr.Node; 024import javax.jcr.NodeIterator; 025import javax.jcr.RepositoryException; 026import javax.jcr.Value; 027import javax.jcr.lock.Lock; 028import javax.jcr.lock.LockManager; 029import javax.jcr.nodetype.NodeType; 030 031import org.apache.avalon.framework.logger.Logger; 032import org.apache.jackrabbit.util.Text; 033 034import org.ametys.plugins.repository.AmetysObject; 035import org.ametys.plugins.repository.AmetysObjectFactory; 036import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 037import org.ametys.plugins.repository.AmetysObjectIterable; 038import org.ametys.plugins.repository.AmetysObjectResolver; 039import org.ametys.plugins.repository.AmetysRepositoryException; 040import org.ametys.plugins.repository.ChainedAmetysObjectIterable; 041import org.ametys.plugins.repository.CollectionIterable; 042import org.ametys.plugins.repository.RepositoryConstants; 043import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 044import org.ametys.plugins.repository.TraversableAmetysObject; 045import org.ametys.plugins.repository.UnknownAmetysObjectException; 046import org.ametys.plugins.repository.virtual.VirtualAmetysObjectFactory; 047 048/** 049 * Helper for implementing {@link TraversableAmetysObject} stored in JCR. 050 */ 051public final class TraversableAmetysObjectHelper 052{ 053 private TraversableAmetysObjectHelper() 054 { 055 // empty 056 } 057 058 /** 059 * Returns the {@link AmetysObject} at the given subPath, 060 * relative to the given {@link DefaultTraversableAmetysObject}. 061 * @param <A> the actual type of {@link AmetysObject}. 062 * @param object the context {@link DefaultTraversableAmetysObject}. 063 * @param factory the corresponding {@link JCRAmetysObjectFactory}. 064 * @param path the sub path. Cannot be <code>null</code>, empty or absolute. 065 * @param resolver the {@link AmetysObjectResolver}. 066 * @param logger a {@link Logger} for traces. 067 * @return the {@link AmetysObject} at the given subPath, 068 * relative to the given {@link DefaultTraversableAmetysObject}. 069 * @throws AmetysRepositoryException if an error occurs. 070 * @throws UnknownAmetysObjectException if no such object exists. 071 */ 072 @SuppressWarnings("unchecked") 073 public static <A extends AmetysObject> A getChild(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, String path, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException, UnknownAmetysObjectException 074 { 075 if (logger.isDebugEnabled()) 076 { 077 logger.debug("Entering DefaultTraversableAmetysObjectFactory.getChild with path=" + path + ", object=" + object); 078 } 079 080 if (path == null || "".equals(path) || path.charAt(0) == '/') 081 { 082 throw new AmetysRepositoryException("Child path cannot be null, empty or absolute"); 083 } 084 085 Node node = object.getNode(); 086 087 try 088 { 089 // instead of going through resolver for each path segment, we first test the nodetype of the sub Node. 090 // If it's same than this factory, there's no need to resolve anything. 091 String[] pathElements = path.split("/"); 092 093 Node contextNode = node; 094 String contextPath = object.getPath(); 095 String contextParentPath = null; 096 097 int i = 0; 098 while (i < pathElements.length) 099 { 100 if (logger.isDebugEnabled()) 101 { 102 logger.debug("contextPath=" + contextPath + ", pathElement=" + pathElements[i]); 103 } 104 105 if (".".equals(pathElements[i]) || "..".equals(pathElements[i])) 106 { 107 throw new AmetysRepositoryException("Path cannot contain segment with . or .."); 108 } 109 110 // handle special characters for local part 111 String jcrName = _escape(pathElements[i]); 112 113 if (contextNode.hasNode(jcrName)) 114 { 115 // the path element corresponds to a JCR Node 116 Node subNode = contextNode.getNode(jcrName); 117 String type = NodeTypeHelper.getNodeTypeName(subNode); 118 119 if (factory.getNodetypes().contains(type)) 120 { 121 if (logger.isDebugEnabled()) 122 { 123 logger.debug("The nodetype is the same as the current factory, no need to go through resolver: " + type); 124 } 125 126 contextParentPath = contextPath; 127 contextPath += "/" + subNode.getName(); 128 contextNode = subNode; 129 i++; 130 } 131 else 132 { 133 return (A) resolver.resolve(contextPath, subNode, _computeSubPath(pathElements, i + 1), false); 134 } 135 } 136 else if (contextNode.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 137 { 138 // the sub node does not exist, but there may be virtual children 139 JCRAmetysObject contextObject = resolver.resolve(contextParentPath, contextNode, null, true); 140 141 if (contextObject == null) 142 { 143 throw new UnknownAmetysObjectException("There's no object at path " + jcrName + " from path " + contextPath); 144 } 145 146 return (A) resolver.resolveVirtualChild(contextObject, _computeSubPath(pathElements, i)); 147 } 148 else 149 { 150 // there's no children 151 throw new UnknownAmetysObjectException("There's no object at path " + jcrName + " from path " + contextPath); 152 } 153 } 154 155 // on est arrivés au bout du subPath, ce qui signifie que tous les descendants sont connus par cette factory 156 return (A) factory.getAmetysObject(contextNode, contextParentPath); 157 } 158 catch (RepositoryException e) 159 { 160 throw new AmetysRepositoryException("Unable to resolve AmetysObject at path " + path + " relative to AmetysObject at path " + object.getPath(), e); 161 } 162 } 163 164 private static String _computeSubPath(String[] pathElements, int beginIndex) 165 { 166 String subPath = null; 167 168 for (int j = beginIndex; j < pathElements.length; j++) 169 { 170 subPath = subPath == null ? pathElements[j] : subPath + "/" + pathElements[j]; 171 } 172 173 return subPath; 174 } 175 176 /** 177 * Returns all children of the given {@link DefaultTraversableAmetysObject}. 178 * @param <A> the actual type of {@link AmetysObject}s 179 * @param object a {@link DefaultTraversableAmetysObject}. 180 * @param factory the corresponding {@link JCRAmetysObjectFactory}. 181 * @param resolver the {@link AmetysObjectResolver}. 182 * @param logger a {@link Logger} for traces. 183 * @return a List containing all children object in the Ametys hierarchy. 184 * @throws AmetysRepositoryException if an error occurs. 185 */ 186 @SuppressWarnings("unchecked") 187 public static <A extends AmetysObject> AmetysObjectIterable<A> getChildren(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException 188 { 189 if (logger.isDebugEnabled()) 190 { 191 logger.debug("Entering DefaultTraversableAmetysObjectFactory.getChildren with object=" + object); 192 } 193 194 try 195 { 196 Node node = object.getNode(); 197 NodeIterator it = node.getNodes(); 198 List<A> children = new ArrayList<>((int) it.getSize()); 199 200 while (it.hasNext()) 201 { 202 Node child = it.nextNode(); 203 String type = NodeTypeHelper.getNodeTypeName(child); 204 205 if (factory.getNodetypes().contains(type)) 206 { 207 // if the node type correspond to the factory, do not go trough resolver 208 children.add((A) factory.getAmetysObject(child, object.getPath())); 209 } 210 else 211 { 212 A obj = resolver.<A>resolve(object.getPath(), child, null, true); 213 214 if (obj != null) 215 { 216 children.add(obj); 217 } 218 } 219 } 220 221 AmetysObjectIterable<A> childrenIt = new CollectionIterable<>(children); 222 223 // on regarde les virtuels 224 if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 225 { 226 AmetysObjectIterable<A> virtualIt = resolver.resolveVirtualChildren(object); 227 228 List<AmetysObjectIterable<A>> chainedList = new ArrayList<>(); 229 chainedList.add(childrenIt); 230 chainedList.add(virtualIt); 231 232 return new ChainedAmetysObjectIterable<>(chainedList); 233 } 234 else 235 { 236 return childrenIt; 237 } 238 } 239 catch (RepositoryException e) 240 { 241 throw new AmetysRepositoryException("Unable to retrieve children", e); 242 } 243 } 244 245 /** 246 * Tests if a given object has a child with a given name. 247 * @param object the context object. 248 * @param name the name to test. 249 * @param ametysFactoryExtensionPoint the {@link AmetysObjectFactoryExtensionPoint}. 250 * @param logger a {@link Logger} for traces. 251 * @return <code>true</code> is the given object has a child with the given name, 252 * <code>false</code> otherwise. 253 * @throws AmetysRepositoryException if an error occurs. 254 */ 255 public static boolean hasChild(JCRTraversableAmetysObject object, String name, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, Logger logger) throws AmetysRepositoryException 256 { 257 if (logger.isDebugEnabled()) 258 { 259 logger.debug("Entering DefaultTraversableAmetysObjectFactory.hasChild with object=" + object + ", name=" + name); 260 } 261 262 if (name == null || "".equals(name) || name.charAt(0) == '/') 263 { 264 throw new AmetysRepositoryException("Child name cannot be null, empty or absolute"); 265 } 266 267 if (".".equals(name) || "..".equals(name)) 268 { 269 throw new AmetysRepositoryException("Child name cannot be . or .."); 270 } 271 272 Node node = object.getNode(); 273 274 try 275 { 276 String jcrName = _escape(name); 277 if (node.hasNode(jcrName)) 278 { 279 if (logger.isDebugEnabled()) 280 { 281 logger.debug("Child node exists: " + jcrName); 282 } 283 284 // if a physical node exists, its an Ametys child if and only if its nodetype is known 285 Node childNode = node.getNode(jcrName); 286 String nodetype = NodeTypeHelper.getNodeTypeName(childNode); 287 return ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) != null; 288 } 289 else if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY)) 290 { 291 if (logger.isDebugEnabled()) 292 { 293 logger.debug("Looking for virtuals..."); 294 } 295 296 // looking at virtuals... 297 Value[] values = node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues(); 298 for (Value value : values) 299 { 300 String virtual = value.getString(); 301 302 VirtualAmetysObjectFactory virtualFactory = _getVirtualFactory(virtual, ametysFactoryExtensionPoint, logger); 303 304 if (virtualFactory.hasChild(object, name)) 305 { 306 return true; 307 } 308 } 309 } 310 311 return false; 312 } 313 catch (RepositoryException e) 314 { 315 throw new AmetysRepositoryException("Unable to test if the underlying Node for object " + object.getId() + " has a child named " + name, e); 316 } 317 } 318 319 private static VirtualAmetysObjectFactory _getVirtualFactory(String id, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, Logger logger) 320 { 321 if (logger.isDebugEnabled()) 322 { 323 logger.debug("Found virtual id: " + id); 324 } 325 326 AmetysObjectFactory factory = ametysFactoryExtensionPoint.getExtension(id); 327 328 if (factory == null) 329 { 330 throw new AmetysRepositoryException("There's no virtual factory for id " + id); 331 } 332 333 if (!(factory instanceof VirtualAmetysObjectFactory)) 334 { 335 throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id); 336 } 337 338 VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory; 339 340 return virtualFactory; 341 } 342 343 /** 344 * Creates a child to the given object. 345 * @param <A> the actual type of {@link AmetysObject}. 346 * @param object the parent {@link AmetysObject}. 347 * @param factory the corresponding {@link JCRAmetysObjectFactory}. 348 * @param name the new object's name. 349 * @param type the new object's type. 350 * @param ametysFactoryExtensionPoint the {@link AmetysObjectFactoryExtensionPoint}. 351 * @param resolver the {@link AmetysObjectResolver}. 352 * @param logger a {@link Logger} for traces. 353 * @return the newly created {@link AmetysObject}. 354 * @throws AmetysRepositoryException if an error occurs. 355 */ 356 @SuppressWarnings("unchecked") 357 public static <A extends AmetysObject> A createChild(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, String name, String type, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException 358 { 359 if (logger.isDebugEnabled()) 360 { 361 logger.debug("Entering DefaultTraversableAmetysObjectFactory.createChild with object=" + object + ", name=" + name + ", type=" + type); 362 } 363 364 // the code of this method is mainly duplicated from the AmetysObjectResolver.createAndResolve method, 365 // with the optimization that there's no need to go through resolver when the nodetype id the same than this factory 366 367 if (ametysFactoryExtensionPoint.getFactoryForNodetype(type) == null) 368 { 369 throw new AmetysRepositoryException("Cannot create a node '" + name + "' under '" + object.getPath() + " (" + object.getId() + ")': There's no factory for nodetype: " + type); 370 } 371 372 Node contextNode = object.getNode(); 373 374 try 375 { 376 _checkLock(contextNode); 377 378 String legalName = _escape(name); 379 Node node = contextNode.addNode(legalName, type); 380 NodeType[] mixinNodeTypes = node.getMixinNodeTypes(); 381 boolean foundMixin = false; 382 383 int i = 0; 384 while (!foundMixin && i < mixinNodeTypes.length) 385 { 386 if (AmetysObjectResolver.OBJECT_TYPE.equals(mixinNodeTypes[i].getName())) 387 { 388 foundMixin = true; 389 } 390 391 i++; 392 } 393 394 if (!foundMixin) 395 { 396 node.addMixin(AmetysObjectResolver.OBJECT_TYPE); 397 } 398 399 if (factory.getNodetypes().contains(type)) 400 { 401 // pas besoin de repasser par le resolver si le type est le même que cette factory 402 return (A) factory.getAmetysObject(node, object.getPath()); 403 } 404 405 return (A) resolver.resolve(object.getPath(), node, null, false); 406 } 407 catch (ItemExistsException e) 408 { 409 throw new RepositoryIntegrityViolationException("The object " + name + " already exist at path " + object.getParentPath(), e); 410 } 411 catch (RepositoryException e) 412 { 413 throw new AmetysRepositoryException("Unable to add child node for the underlying node for object " + object.getId(), e); 414 } 415 } 416 417 private static String _escape(String qName) 418 { 419 int index = qName.indexOf(':'); 420 421 if (index == -1) 422 { 423 return Text.escapeIllegalJcrChars(qName); 424 } 425 else 426 { 427 return qName.substring(0, index) + ':' + Text.escapeIllegalJcrChars(qName.substring(index + 1, qName.length())); 428 } 429 } 430 431 private static void _checkLock(Node node) throws RepositoryException 432 { 433 if (node.isLocked()) 434 { 435 LockManager lockManager = node.getSession().getWorkspace().getLockManager(); 436 Lock lock = lockManager.getLock(node.getPath()); 437 Node lockHolder = lock.getNode(); 438 439 lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString()); 440 } 441 } 442}