001/* 002 * Copyright 2015 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.workspaces.repository.jcr; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import javax.jcr.ItemNotFoundException; 028import javax.jcr.Node; 029import javax.jcr.PathNotFoundException; 030import javax.jcr.Repository; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033import javax.jcr.Workspace; 034import javax.jcr.lock.LockManager; 035import javax.jcr.nodetype.NodeDefinition; 036 037import org.apache.avalon.framework.component.Component; 038import org.apache.avalon.framework.logger.AbstractLogEnabled; 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; 043 044import org.ametys.core.ui.Callable; 045import org.ametys.plugins.repositoryapp.RepositoryProvider; 046import org.ametys.runtime.config.Config; 047 048/** 049 * Component providing methods to access the repository. 050 */ 051public class RepositoryDao extends AbstractLogEnabled implements Component, Serviceable 052{ 053 054 /** The repository provider. */ 055 protected RepositoryProvider _repositoryProvider; 056 057 /** The node state tracker. */ 058 protected NodeStateTracker _nodeStateTracker; 059 060 /** The node type hierarchy component. */ 061 protected NodeTypeHierarchyComponent _nodeTypeHierarchy; 062 063 private ServiceManager _serviceManager; 064 065 @Override 066 public void service(ServiceManager serviceManager) throws ServiceException 067 { 068 _serviceManager = serviceManager; 069 _repositoryProvider = (RepositoryProvider) serviceManager.lookup(RepositoryProvider.ROLE); 070 _nodeStateTracker = (NodeStateTracker) serviceManager.lookup(NodeStateTracker.ROLE); 071 _nodeTypeHierarchy = (NodeTypeHierarchyComponent) serviceManager.lookup(NodeTypeHierarchyComponent.ROLE); 072 } 073 074 /** 075 * Get information on the repository. 076 * @return information on the repository as a Map. 077 */ 078 @Callable 079 public Map<String, Object> getRepositoryInfo() 080 { 081 Map<String, Object> info = new HashMap<>(); 082 083 info.put("standalone", _serviceManager.hasService(Repository.class.getName())); 084 085 // Must be compatible with safe mode 086 Config configInstance = Config.getInstance(); 087 if (configInstance != null) 088 { 089 String defaultOrder = configInstance.getValue("repository.default.sort"); 090 info.put("defaultOrder", defaultOrder); 091 } 092 093 return info; 094 } 095 096 /** 097 * Get the list of available workspaces in the repository. 098 * @return the list of available workspaces in the repository. 099 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 100 */ 101 @Callable 102 public List<String> getWorkspaces() throws RepositoryException 103 { 104 Session session = _repositoryProvider.getSession("default"); 105 106 Workspace workspace = session.getWorkspace(); 107 108 return Arrays.asList(workspace.getAccessibleWorkspaceNames()); 109 } 110 111 /** 112 * Get node information by path. 113 * @param paths the node paths, relative to the root node (without leading slash). 114 * @param workspaceName the workspace name. 115 * @return information on the node as a Map. 116 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 117 */ 118 @Callable 119 public Map<String, Object> getNodesByPath(Collection<String> paths, String workspaceName) throws RepositoryException 120 { 121 Session session = _repositoryProvider.getSession(workspaceName); 122 Node rootNode = session.getRootNode(); 123 124 List<Map<String, Object>> nodes = new ArrayList<>(); 125 List<String> notFound = new ArrayList<>(); 126 127 for (String path : paths) 128 { 129 try 130 { 131 Node node = null; 132 133 String relPath = removeLeadingSlash(path); 134 if (StringUtils.isEmpty(relPath)) 135 { 136 node = rootNode; 137 } 138 else 139 { 140 node = rootNode.getNode(relPath); 141 } 142 143 Map<String, Object> nodeInfo = new HashMap<>(); 144 fillNodeInfo(node, nodeInfo); 145 nodes.add(nodeInfo); 146 } 147 catch (PathNotFoundException e) 148 { 149 notFound.add(path); 150 } 151 } 152 153 Map<String, Object> result = new HashMap<>(); 154 result.put("nodes", nodes); 155 result.put("notFound", notFound); 156 157 return result; 158 } 159 160 /** 161 * Get node information by path. 162 * @param path the node path, relative to the root node (without leading slash). 163 * @param workspaceName the workspace name. 164 * @return information on the node as a Map. 165 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 166 */ 167 @Callable 168 public Map<String, Object> getNodeByPath(String path, String workspaceName) throws RepositoryException 169 { 170 Session session = _repositoryProvider.getSession(workspaceName); 171 String relPath = removeLeadingSlash(path); 172 Node node = session.getRootNode().getNode(relPath); 173 174 Map<String, Object> nodeInfo = new HashMap<>(); 175 176 fillNodeInfo(node, nodeInfo); 177 178 return nodeInfo; 179 } 180 181 /** 182 * Get node information by its identifier. 183 * @param identifier the node identifier. 184 * @param workspaceName the workspace name. 185 * @return information on the node as a Map. 186 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 187 */ 188 @Callable 189 public Map<String, Object> getNodeByIdentifier(String identifier, String workspaceName) throws RepositoryException 190 { 191 Session session = _repositoryProvider.getSession(workspaceName); 192 try 193 { 194 Node node = session.getNodeByIdentifier(identifier); 195 196 Map<String, Object> nodeInfo = new HashMap<>(); 197 198 fillNodeInfo(node, nodeInfo); 199 200 return nodeInfo; 201 } 202 catch (ItemNotFoundException e) 203 { 204 if (getLogger().isWarnEnabled()) 205 { 206 getLogger().warn(String.format("Item '%s' not found in workspace '%s'", identifier, workspaceName), e); 207 } 208 209 return null; 210 } 211 } 212 213 /** 214 * Get the possible children types of a node. 215 * @param nodePath The node path. 216 * @param workspaceName The workspace name. 217 * @return the possible children types of the node. 218 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 219 */ 220 @Callable 221 public Set<String> getChildrenTypes(String nodePath, String workspaceName) throws RepositoryException 222 { 223 Session session = _repositoryProvider.getSession(workspaceName); 224 225 String relPath = removeLeadingSlash(nodePath); 226 227 Node node = null; 228 if (StringUtils.isEmpty(relPath)) 229 { 230 node = session.getRootNode(); 231 } 232 else 233 { 234 node = session.getRootNode().getNode(relPath); 235 } 236 237 // Store allowed types in this set 238 Set<String> availableChildrenTypes = new HashSet<>(); 239 NodeDefinition[] childNodeDefinitions = node.getPrimaryNodeType().getChildNodeDefinitions(); 240 for (NodeDefinition nodeDef : childNodeDefinitions) 241 { 242 availableChildrenTypes.addAll(_nodeTypeHierarchy.getAvailableChildrenTypes(nodeDef, workspaceName)); 243 } 244 245 return availableChildrenTypes; 246 } 247 248 /** 249 * Add a node. 250 * @param parentPath the parent node path. 251 * @param childName the name of the node to create. 252 * @param childType the type of the node to create. 253 * @param workspaceName the workspace name. 254 * @return A Map with information on the created node. 255 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 256 */ 257 @Callable 258 public Map<String, Object> addNode(String parentPath, String childName, String childType, String workspaceName) throws RepositoryException 259 { 260 if (getLogger().isDebugEnabled()) 261 { 262 getLogger().debug("Trying to add child: '" + childName + "' to the node at path: '" + parentPath + "'"); 263 } 264 265 Session session = _repositoryProvider.getSession(workspaceName); 266 String relPath = removeLeadingSlash(parentPath); 267 268 // Get the parent node 269 Node parentNode = null; 270 if (StringUtils.isEmpty(relPath)) 271 { 272 parentNode = session.getRootNode(); 273 } 274 else 275 { 276 parentNode = session.getRootNode().getNode(relPath); 277 } 278 279 // Add the new child to the parent 280 Node childNode = parentNode.addNode(childName, childType); 281 282 String fullPath = NodeGroupHelper.getPathWithGroups(childNode); 283 284 Map<String, Object> result = new HashMap<>(); 285 result.put("path", childNode.getPath()); 286 result.put("pathWithGroups", fullPath); 287 288 _nodeStateTracker.nodeAdded(workspaceName, fullPath); 289 290 return result; 291 } 292 293 /** 294 * Remove a node from the repository. 295 * @param path The absolute node path, can start with a slash or not. 296 * @param workspaceName The workspace name. 297 * @return The full parent path. 298 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 299 */ 300 @Callable 301 public String removeNode(String path, String workspaceName) throws RepositoryException 302 { 303 Session session = _repositoryProvider.getSession(workspaceName); 304 305 if (getLogger().isDebugEnabled()) 306 { 307 getLogger().debug("Trying to remove node at path: '" + path + "'"); 308 } 309 310 String relPath = removeLeadingSlash(path); 311 312 // Get and remove the node. 313 Node node = session.getRootNode().getNode(relPath); 314 Node parentNode = node.getParent(); 315 316 String fullPath = NodeGroupHelper.getPathWithGroups(node); 317 String fullParentPath = NodeGroupHelper.getPathWithGroups(parentNode); 318 319 node.remove(); 320 321 _nodeStateTracker.nodeRemoved(workspaceName, fullPath); 322 _nodeStateTracker.nodeAdded(workspaceName, fullParentPath); 323 324 return fullParentPath; 325 } 326 327 /** 328 * Remove a property from a node. 329 * @param path The absolute node path, can start with a slash or not. 330 * @param workspaceName The workspace name. 331 * @param propertyName The name of the property to remove. 332 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 333 */ 334 @Callable 335 public void removeProperty(String path, String workspaceName, String propertyName) throws RepositoryException 336 { 337 if (getLogger().isDebugEnabled()) 338 { 339 getLogger().debug("Removing property '" + propertyName + "' from the node at path '" + path + "'"); 340 } 341 342 Session session = _repositoryProvider.getSession(workspaceName); 343 String relPath = removeLeadingSlash(path); 344 345 Node node = null; 346 if (StringUtils.isEmpty(relPath)) 347 { 348 node = session.getRootNode(); 349 } 350 else 351 { 352 node = session.getRootNode().getNode(relPath); 353 } 354 355 // Remove the property 356 node.getProperty(propertyName).remove(); 357 } 358 359 /** 360 * Unlock a node. 361 * @param path The absolute node path. 362 * @param workspaceName The workspace name. 363 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 364 */ 365 @Callable 366 public void unlockNode(String path, String workspaceName) throws RepositoryException 367 { 368 if (getLogger().isDebugEnabled()) 369 { 370 getLogger().debug("Trying to unlock the node at path '" + path + "'"); 371 } 372 373 Session session = _repositoryProvider.getSession(workspaceName); 374 LockManager lockManager = session.getWorkspace().getLockManager(); 375 376 try 377 { 378 // Try to add lock token stored on AmetysObject 379 Node node = session.getNode(path); 380 if (node.hasProperty("ametys-internal:lockToken")) 381 { 382 String lockToken = node.getProperty("ametys-internal:lockToken").getString(); 383 lockManager.addLockToken(lockToken); 384 } 385 else if (getLogger().isInfoEnabled()) 386 { 387 getLogger().info("Lock token property not found for node at path '" + path + "'"); 388 } 389 } 390 catch (RepositoryException e) 391 { 392 getLogger().warn("Unable to add locken token to unlock node at path '" + path + "'", e); 393 } 394 395 session.getWorkspace().getLockManager().unlock(path); 396 } 397 398 /** 399 * Check-out a node. 400 * @param path The absolute node path, must start with a slash. 401 * @param workspaceName The workspace name. 402 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 403 */ 404 @Callable 405 public void checkoutNode(String path, String workspaceName) throws RepositoryException 406 { 407 if (getLogger().isDebugEnabled()) 408 { 409 getLogger().debug("Trying to checkout the node at path '" + path + "'"); 410 } 411 412 Session session = _repositoryProvider.getSession(workspaceName); 413 414 // Check the node out. 415 session.getWorkspace().getVersionManager().checkout(path); 416 } 417 418 /** 419 * Save a session. 420 * @param workspaceName The workspace name. 421 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 422 */ 423 @Callable 424 public void saveSession(String workspaceName) throws RepositoryException 425 { 426 if (getLogger().isDebugEnabled()) 427 { 428 getLogger().debug("Persisting session for workspace '" + workspaceName + "'"); 429 } 430 431 Session session = _repositoryProvider.getSession(workspaceName); 432 433 session.save(); 434 435 _nodeStateTracker.clear(workspaceName); 436 } 437 438 /** 439 * Rollback a session. 440 * @param workspaceName The workspace name. 441 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 442 */ 443 @Callable 444 public void rollbackSession(String workspaceName) throws RepositoryException 445 { 446 if (getLogger().isDebugEnabled()) 447 { 448 getLogger().debug("Rolling back session for workspace '" + workspaceName + "'"); 449 } 450 451 Session session = _repositoryProvider.getSession(workspaceName); 452 453 session.refresh(false); 454 455 _nodeStateTracker.clear(workspaceName); 456 } 457 458 /** 459 * Fill the node info. 460 * @param node The node to convert 461 * @param nodeInfo The map to fill 462 * @throws RepositoryException if an error occurs getting or setting data from/in the repository. 463 */ 464 protected void fillNodeInfo(Node node, Map<String, Object> nodeInfo) throws RepositoryException 465 { 466 boolean hasOrderableChildNodes = true; 467 468 nodeInfo.put("id", node.getIdentifier()); 469 nodeInfo.put("path", node.getPath()); 470 nodeInfo.put("pathWithGroups", NodeGroupHelper.getPathWithGroups(node)); 471 nodeInfo.put("name", node.getName()); 472 nodeInfo.put("index", node.getIndex()); 473 nodeInfo.put("hasOrderableChildNodes", hasOrderableChildNodes); 474 nodeInfo.put("locked", node.isLocked()); 475 nodeInfo.put("checkedOut", node.isCheckedOut()); 476 } 477 478 /** 479 * Add a leading slash to the path. 480 * @param path the path. 481 * @return the path with a leading slash. 482 */ 483 public static String addLeadingSlash(String path) 484 { 485 if (StringUtils.isNotEmpty(path) && path.charAt(0) != '/') 486 { 487 return '/' + path; 488 } 489 return path; 490 } 491 492 /** 493 * Remove the leading slash from the path if needed. 494 * @param path the path. 495 * @return the path without leading slash. 496 */ 497 public static String removeLeadingSlash(String path) 498 { 499 if (StringUtils.isNotEmpty(path) && path.charAt(0) == '/') 500 { 501 return path.substring(1); 502 } 503 return path; 504 } 505}