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