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 */
016package org.ametys.plugins.explorer.cmis;
017
018import java.util.ArrayList;
019import java.util.Collection;
020
021import javax.jcr.Node;
022import javax.jcr.RepositoryException;
023import javax.jcr.nodetype.ConstraintViolationException;
024
025import org.apache.chemistry.opencmis.client.api.CmisObject;
026import org.apache.chemistry.opencmis.client.api.Document;
027import org.apache.chemistry.opencmis.client.api.Folder;
028import org.apache.chemistry.opencmis.client.api.ItemIterable;
029import org.apache.chemistry.opencmis.client.api.Session;
030import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
031import org.apache.commons.lang3.StringUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import org.ametys.plugins.explorer.ExplorerNode;
036import org.ametys.plugins.explorer.resources.ResourceCollection;
037import org.ametys.plugins.repository.AbstractAmetysObject;
038import org.ametys.plugins.repository.AmetysObject;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.repository.CollectionIterable;
041import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
042import org.ametys.plugins.repository.UnknownAmetysObjectException;
043import org.ametys.plugins.repository.data.ametysobject.ModifiableModelLessDataAwareAmetysObject;
044import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
045import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
046import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
047import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
048import org.ametys.plugins.repository.jcr.JCRAmetysObject;
049import org.ametys.plugins.repository.jcr.SimpleAmetysObjectFactory;
050import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
051import org.ametys.plugins.repository.metadata.jcr.JCRCompositeMetadata;
052
053/**
054 * {@link AmetysObject} implementing the root of {@link CMISResourcesCollection}s
055 */
056public class CMISRootResourcesCollection extends AbstractAmetysObject implements JCRAmetysObject, ResourceCollection, ModifiableModelLessDataAwareAmetysObject
057{
058    /** application id for resources collections. */
059    public static final String APPLICATION_ID = "Ametys.plugins.explorer.applications.resources.Resources";
060   
061    /** Metadata for repository id */
062    public static final String DATA_REPOSITORY_ID = "repositoryId";
063    /** Metadata for repository url */
064    public static final String DATA_REPOSITORY_URL = "repositoryUrl";
065    /** Attribute for repository root path */
066    public static final String DATA_MOUNT_POINT = "mountPoint";
067    /** Metadata for user login */
068    public static final String DATA_USER = "user";
069    /** Metadata for user password */
070    public static final String DATA_PASSWORD = "password";
071   
072    private static final Logger __LOGGER = LoggerFactory.getLogger(CMISResourcesCollection.class);
073    
074    /** The corresponding {@link SimpleAmetysObjectFactory} */
075    private final CMISTreeFactory _factory;
076    
077    private final Node _node;
078    
079    private String _name;
080    private String _parentPath;
081    
082    private Session _session;
083    private Folder _root;
084    
085    /**
086     * Creates a {@link CMISRootResourcesCollection}.
087     * @param node the node backing this {@link AmetysObject}
088     * @param parentPath the parentPath in the Ametys hierarchy
089     * @param factory the CMISRootResourcesCollectionFactory which created this AmetysObject
090     */
091    public CMISRootResourcesCollection(Node node, String parentPath, CMISTreeFactory factory)
092    {
093        _node = node;
094        _parentPath = parentPath;
095        _factory = factory;
096        
097        try
098        {
099            _name = _node.getName();
100        }
101        catch (RepositoryException e)
102        {
103            throw new AmetysRepositoryException("Unable to get node name", e);
104        }
105    }
106    
107    void connect(Session session, Folder root)
108    {
109        _session = session;
110        _root = root;
111    }
112    
113    Session getSession()
114    {
115        return _session;
116    }
117
118    Folder getRootFolder()
119    {
120        return _root;
121    }
122
123    @SuppressWarnings("unchecked")
124    @Override
125    public AmetysObject getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
126    {
127        if (_session == null)
128        {
129            throw new UnknownAmetysObjectException("Failed to connect to CMIS server");
130        }
131        
132        String mountPoint = StringUtils.defaultIfBlank(getMountPoint(), "/");
133        // ensure mount point start with "/" (Chemistry restriction)
134        if (!mountPoint.startsWith("/"))
135        {
136            mountPoint = "/" + mountPoint;
137        }
138        // add trailing slash if needed
139        if (!mountPoint.endsWith("/"))
140        {
141            mountPoint = mountPoint + "/";
142        }
143        
144        String remotePath = mountPoint + path;
145        // ensure path does not ends with "/" (Chemistry restriction)
146        if (path.endsWith("/") && remotePath.length() != 1)
147        {
148            remotePath = remotePath.substring(0, remotePath.length() - 1);
149        }
150        
151        CmisObject entry = _session.getObjectByPath(remotePath);
152        CmisObject object = _session.getObject(entry);
153        
154        BaseTypeId baseTypeId = object.getBaseType().getBaseTypeId();
155        
156        if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER))
157        {
158            return new CMISResourcesCollection((Folder) object, this, null);
159        }
160        else if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT))
161        {
162            return new CMISResource((Document) object, this, null);
163        }
164        else
165        {
166            throw new UnknownAmetysObjectException("Unhandled CMIS type '" + baseTypeId + "', cannot get child at path " + path);
167        }
168    }
169
170    @Override
171    public CollectionIterable<AmetysObject> getChildren() throws AmetysRepositoryException
172    {
173        Collection<AmetysObject> aoChildren = new ArrayList<>(); 
174        
175        if (_session == null)
176        {
177            return new CollectionIterable<>(aoChildren);
178        }
179        
180        ItemIterable<CmisObject> children = _root.getChildren();
181        
182        for (CmisObject child : children)
183        {
184            BaseTypeId typeId = child.getBaseTypeId();
185            
186            if (typeId.equals(BaseTypeId.CMIS_FOLDER))
187            {
188                aoChildren.add(new CMISResourcesCollection((Folder) child, this, this));
189            }
190            else if (typeId.equals(BaseTypeId.CMIS_DOCUMENT))
191            {
192                Document cmisDoc = (Document) child;
193                
194                // Check if CMIS document has content, if not ignore it
195                if (StringUtils.isNotEmpty(cmisDoc.getContentStreamFileName()))
196                {
197                    aoChildren.add(new CMISResource(cmisDoc, this, this));
198                }
199            }
200            else
201            {
202                __LOGGER.warn("Unhandled CMIS type {}. It will be ignored.", typeId);
203            }
204        }
205
206        return new CollectionIterable<>(aoChildren);
207    }
208
209    @Override
210    public boolean hasChild(String name) throws AmetysRepositoryException
211    {
212        if (_session == null)
213        {
214            return false;
215        }
216        
217        ItemIterable<CmisObject> children = _root.getChildren();
218        for (CmisObject child : children)
219        {
220            if (child.getName().equals(name))
221            {
222                return true;
223            }
224        }
225        
226        return false;
227    }
228    
229    public ModifiableModelLessDataHolder getDataHolder()
230    {
231        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode());
232        return new DefaultModifiableModelLessDataHolder(_factory.getDataTypesExtensionPoint(), repositoryData);
233    }
234    
235    @Override
236    public ModifiableCompositeMetadata getMetadataHolder()
237    {
238        return new JCRCompositeMetadata(getNode(), _factory._resolver);
239    }
240    
241    @Override
242    public String getIconCls()
243    {
244        if (_session == null)
245        {
246            return "ametysicon-share40 decorator-ametysicon-sign-caution a-tree-decorator-error-color";
247        }
248        
249        String productName = _session.getRepositoryInfo().getProductName();
250        if (productName.toLowerCase().indexOf("alfresco") != -1)
251        {
252            // FIXME EXPLORER-494 Use a dedicated Alfresco glyph
253            return "ametysicon-share40";
254        }
255        else if (productName.toLowerCase().indexOf("nuxeo") != -1)
256        {
257            // FIXME EXPLORER-494 Use a dedicated Nuxeo glyph
258            return "ametysicon-share40";
259        }
260        
261        // FIXME EXPLORER-494 Use a dedicated CMIS glyph
262        return "ametysicon-share40";
263    }
264
265    @Override
266    public String getApplicationId()
267    {
268        return APPLICATION_ID;
269    }
270
271    @Override
272    public String getName() throws AmetysRepositoryException
273    {
274        return _name;
275    }
276
277    @Override
278    public String getParentPath() throws AmetysRepositoryException
279    {
280        if (_parentPath == null)
281        {
282            _parentPath = getParent().getPath();
283        }
284        
285        return _parentPath;
286    }
287
288    @Override
289    public String getPath() throws AmetysRepositoryException
290    {
291        return getParentPath() + "/" + getName();
292    }
293    
294    @Override
295    public Node getNode()
296    {
297        return _node;
298    }
299    
300    @Override
301    public String getId()
302    {
303        try
304        {
305            return _factory.getScheme() + "://" + _node.getIdentifier();
306        }
307        catch (RepositoryException e)
308        {
309            throw new AmetysRepositoryException("Unable to get node UUID", e);
310        }
311    }
312    
313    public boolean hasChildResources() throws AmetysRepositoryException
314    {
315        // we don't actually know if there are children or not, 
316        // but it's an optimization to don't make another CMIS request
317        return true;
318    }
319    
320    public boolean hasChildExplorerNodes() throws AmetysRepositoryException
321    {
322        // we don't actually know if there are children or not, 
323        // but it's an optimization to don't make another CMIS request
324        return true;
325    }
326    
327    @Override
328    public void rename(String newName) throws AmetysRepositoryException
329    {
330        try
331        {
332            getNode().getSession().move(getNode().getPath(), getNode().getParent().getPath() + "/" + newName);
333        }
334        catch (RepositoryException e)
335        {
336            throw new AmetysRepositoryException(e);
337        }
338    }
339
340    @Override
341    public void remove() throws AmetysRepositoryException, RepositoryIntegrityViolationException
342    {
343        try
344        {
345            getNode().remove();
346        }
347        catch (ConstraintViolationException e)
348        {
349            throw new RepositoryIntegrityViolationException(e);
350        }
351        catch (RepositoryException e)
352        {
353            throw new AmetysRepositoryException(e);
354        }
355    }
356
357    @SuppressWarnings("unchecked")
358    @Override
359    public <A extends AmetysObject> A getParent() throws AmetysRepositoryException
360    {
361        return (A) _factory.getParent(this);
362    }
363
364    @Override
365    public void saveChanges() throws AmetysRepositoryException
366    {
367        try
368        {
369            getNode().getSession().save();
370        }
371        catch (javax.jcr.RepositoryException e)
372        {
373            throw new AmetysRepositoryException("Unable to save changes", e);
374        }
375    }
376    
377    @Override
378    public void revertChanges() throws AmetysRepositoryException
379    {
380        try
381        {
382            getNode().refresh(false);
383        }
384        catch (javax.jcr.RepositoryException e)
385        {
386            throw new AmetysRepositoryException("Unable to revert changes.", e);
387        }
388    }
389    
390    @Override
391    public boolean needsSave() throws AmetysRepositoryException
392    {
393        try
394        {
395            return _node.getSession().hasPendingChanges();
396        }
397        catch (RepositoryException e)
398        {
399            throw new AmetysRepositoryException(e);
400        }
401    }
402    
403    @Override
404    public String getResourcePath() throws AmetysRepositoryException
405    {
406        return getExplorerPath();
407    }
408    
409    @Override
410    public String getExplorerPath()
411    {
412        AmetysObject parent = getParent();
413        
414        if (parent instanceof ExplorerNode)
415        {
416            return ((ExplorerNode) parent).getExplorerPath() + "/" + getName();
417        }
418        else
419        {
420            return "";
421        }
422    }
423    
424    /**
425     * Get the user to connect to CMIS repository
426     * @return the user login
427     * @throws AmetysRepositoryException if an error occurred
428     */
429    public String getUser() throws AmetysRepositoryException
430    {
431        return getValue(DATA_USER);
432    }
433    
434    /**
435     * Get the password to connect to CMIS repository
436     * @return the user password
437     * @throws AmetysRepositoryException if an error occurred
438     */
439    public String getPassword() throws AmetysRepositoryException
440    {
441        return getValue(DATA_PASSWORD);
442    }
443    
444    /**
445     * Get the CMIS repository URL
446     * @return the CMIS repository URL
447     * @throws AmetysRepositoryException if an error occurred
448     */
449    public String getRepositoryUrl() throws AmetysRepositoryException
450    {
451        return getValue(DATA_REPOSITORY_URL);
452    }
453    
454    /**
455     * Get the CMIS repository id
456     * @return the CMIS repository id
457     * @throws AmetysRepositoryException if an error occurred
458     */
459    public String getRepositoryId () throws AmetysRepositoryException
460    {
461        return getValue(DATA_REPOSITORY_ID);
462    }
463    
464    /**
465     * Get the CMIS mount point
466     * @return the mount point
467     * @throws AmetysRepositoryException if an error occurred
468     */
469    public String getMountPoint() throws AmetysRepositoryException
470    {
471        return getValue(DATA_MOUNT_POINT);
472    }
473    
474    /**
475     * Set the URL of the CMIS repository
476     * @param url the CMIS repository URL
477     * @throws AmetysRepositoryException if an error occurred
478     */
479    public void setRepositoryUrl(String url) throws AmetysRepositoryException
480    {
481        setValue(DATA_REPOSITORY_URL, url);
482    }
483    
484    /**
485     * Set the id of the CMIS repository
486     * @param id the CMIS repository id
487     * @throws AmetysRepositoryException if an error occurred
488     */
489    public void setRepositoryId(String id) throws AmetysRepositoryException
490    {
491        setValue(DATA_REPOSITORY_ID, id);
492    }
493    
494    /**
495     * Set a mount point for the CMIS Repository
496     * @param mountPoint the mount point path
497     */
498    public void setMountPoint(String mountPoint) 
499    {
500        setValue(DATA_MOUNT_POINT, mountPoint);
501    }
502    
503    /**
504     * Set a user name for the CMIS Repository
505     * @param user the login
506     */
507    public void setUser(String user) 
508    {
509        setValue(DATA_USER, user);
510    }
511    
512    /**
513     * Set a password for the CMIS Repository
514     * @param password the password
515     */
516    public void setPassword(String password) 
517    {
518        setValue(DATA_PASSWORD, password);
519    }
520    
521    @Override
522    public String getDescription()
523    {
524        return null;
525    }
526}