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}