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.Collection;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import javax.jcr.ItemNotFoundException;
025import javax.jcr.Node;
026import javax.jcr.Repository;
027import javax.jcr.RepositoryException;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.configuration.Configurable;
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.chemistry.opencmis.client.api.CmisObject;
037import org.apache.chemistry.opencmis.client.api.Document;
038import org.apache.chemistry.opencmis.client.api.Folder;
039import org.apache.chemistry.opencmis.client.api.ObjectId;
040import org.apache.chemistry.opencmis.client.api.Session;
041import org.apache.chemistry.opencmis.client.api.SessionFactory;
042import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
043import org.apache.chemistry.opencmis.commons.SessionParameter;
044import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
045import org.apache.chemistry.opencmis.commons.enums.BindingType;
046import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
047import org.apache.chemistry.opencmis.commons.exceptions.CmisConnectionException;
048import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
049import org.apache.commons.lang3.StringUtils;
050import org.apache.commons.lang3.Strings;
051
052import org.ametys.core.cache.AbstractCacheManager;
053import org.ametys.core.cache.Cache;
054import org.ametys.core.observation.Event;
055import org.ametys.core.observation.ObservationManager;
056import org.ametys.core.observation.Observer;
057import org.ametys.plugins.explorer.ObservationConstants;
058import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection;
059import org.ametys.plugins.repository.AmetysObject;
060import org.ametys.plugins.repository.AmetysObjectResolver;
061import org.ametys.plugins.repository.AmetysRepositoryException;
062import org.ametys.plugins.repository.RepositoryConstants;
063import org.ametys.plugins.repository.UnknownAmetysObjectException;
064import org.ametys.plugins.repository.data.type.ModelItemTypeExtensionPoint;
065import org.ametys.plugins.repository.jcr.JCRAmetysObjectFactory;
066import org.ametys.plugins.repository.provider.AbstractRepository;
067import org.ametys.runtime.i18n.I18nizableText;
068import org.ametys.runtime.plugin.component.AbstractLogEnabled;
069
070/**
071 * Create the Root of CMIS Resources Collections
072 */
073public class CMISTreeFactory extends AbstractLogEnabled implements JCRAmetysObjectFactory<AmetysObject>, Configurable, Serviceable, Initializable, Observer
074{
075    /** Nodetype for resources collection */
076    public static final String CMIS_ROOT_COLLECTION_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":cmis-root-collection";
077    
078    private static final String __SESSION_CACHE = CMISTreeFactory.class.getName() + "$cmisSessionCache";
079    
080    /** The application {@link AmetysObjectResolver} */
081    protected AmetysObjectResolver _resolver;
082    
083    /** The configured scheme */
084    protected String _scheme;
085
086    /** The configured nodetype */
087    protected String _nodetype;
088    
089    /** JCR Repository */
090    protected Repository _repository;
091    
092    private ObservationManager _observationManager;
093
094    private AbstractCacheManager _cacheManager;
095    
096    private ModelItemTypeExtensionPoint _basicTypesExtensionPoint;
097    
098    @Override
099    public void service(ServiceManager manager) throws ServiceException
100    {
101        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
102        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
103        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
104        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
105        _basicTypesExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_BASIC);
106    }
107
108    @Override
109    public void configure(Configuration configuration) throws ConfigurationException
110    {
111        _scheme = configuration.getChild("scheme").getValue();
112        
113        Configuration[] nodetypesConf = configuration.getChildren("nodetype");
114        
115        if (nodetypesConf.length != 1)
116        {
117            throw new ConfigurationException("A SimpleAmetysObjectFactory must have one and only one associated nodetype. "
118                                           + "The '" + configuration.getAttribute("id") + "' component has " + nodetypesConf.length);
119        }
120        
121        _nodetype = nodetypesConf[0].getValue();
122    }
123    
124    public void initialize() throws Exception
125    {
126        _observationManager.registerObserver(this);
127        _cacheManager.createMemoryCache(__SESSION_CACHE,
128                new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_CACHE_CMIS_SESSION_LABEL"),
129                new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_CACHE_CMIS_SESSION_DESCRIPTION"),
130                true,
131                null);
132    }
133
134    @Override
135    public CMISRootResourcesCollection getAmetysObject(Node node, String parentPath) throws AmetysRepositoryException, RepositoryException
136    {
137        CMISRootResourcesCollection root = new CMISRootResourcesCollection(node, parentPath, this);
138        
139        if (!root.hasValue(CMISRootResourcesCollection.DATA_REPOSITORY_URL))
140        {
141            // Object just created, can't connect right now
142            return root;
143        }
144        
145        try
146        {
147            Session session = getAtomPubSession(root);
148            
149            Folder rootFolder = null;
150            if (session != null)
151            {
152                String mountPoint = root.getMountPoint();
153                // mount point is the root folder
154                if (StringUtils.isBlank(mountPoint) || Strings.CS.equals(mountPoint, "/"))
155                {
156                    rootFolder = session.getRootFolder();
157                }
158                // any other valid mount point
159                else if (StringUtils.isNotBlank(mountPoint) && Strings.CS.startsWith(mountPoint, "/"))
160                {
161                    try
162                    {
163                        rootFolder = (Folder) session.getObjectByPath(mountPoint);
164                    }
165                    catch (CmisObjectNotFoundException e)
166                    {
167                        getLogger().error("The mount point '{}' can't be found in the remote repository {}", mountPoint, root.getRepositoryId(), e);
168                    }
169                }
170                
171                // the mount point is valid
172                if (rootFolder != null)
173                {
174                    root.connect(session, rootFolder);
175                }
176            }
177        }
178        catch (CmisConnectionException e)
179        {
180            getLogger().error("Connection to CMIS Atom Pub service failed", e);
181        }
182        catch (CmisObjectNotFoundException e)
183        {
184            getLogger().error("The CMIS Atom Pub service url refers to a non-existent repository", e);
185        }
186        catch (CmisBaseException e)
187        {
188            // all others CMIS errors
189            getLogger().error("An error occured during call of CMIS Atom Pub service", e);
190        }
191        
192        return root;
193    }
194
195    @Override
196    public AmetysObject getAmetysObjectById(String id) throws AmetysRepositoryException
197    {
198        try
199        {
200            // l'id est de la forme <scheme>://uuid(/<cmis_id)
201            String uuid = id.substring(getScheme().length() + 3);
202            int index = uuid.indexOf("/");
203            
204            if (index != -1)
205            {
206                CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index));
207                Session session = root.getSession();
208                if (session == null)
209                {
210                    throw new UnknownAmetysObjectException("Connection to CMIS server failed");
211                }
212                
213                ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
214                CmisObject cmisObject = session.getObject(cmisID);
215                // make sure the object is not stall when resolving
216                cmisObject.refresh();
217                
218                BaseTypeId baseTypeId = cmisObject.getBaseTypeId();
219    
220                if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER))
221                {
222                    return new CMISResourcesCollection((Folder) cmisObject, root, null);
223                }
224                else if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT))
225                {
226                    Document cmisDoc = (Document) cmisObject;
227                    try
228                    {
229                        // EXPLORER-243 alfresco's id point to a version, not to the real "live" document
230                        if (!cmisDoc.isLatestVersion())
231                        {
232                            cmisDoc = cmisDoc.getObjectOfLatestVersion(false);
233                        }
234                    }
235                    catch (CmisBaseException e)
236                    {
237                        // EXPLORER-269 does nothing, nuxeo sometimes throws a CmisRuntimeException here
238                    }
239                    
240                    return new CMISResource(cmisDoc, root, null);
241                }
242                else
243                {
244                    throw new IllegalArgumentException("Unhandled CMIS type: " + baseTypeId);
245                }
246            }
247            else
248            {
249                return getCMISRootResourceCollection (id);
250            }
251        }
252        catch (CmisObjectNotFoundException e)
253        {
254            throw new UnknownAmetysObjectException("No CMIS object found with id " + id, e);
255        }
256        catch (CmisBaseException e)
257        {
258            throw new AmetysRepositoryException("An error occurred while retriving CMIS object '" + id + "'.", e);
259        }
260    }
261    
262    @Override
263    public AmetysObject getAmetysObjectById(String id, javax.jcr.Session session) throws AmetysRepositoryException, RepositoryException
264    {
265        return getAmetysObjectById(id);
266    }
267    
268    /**
269     * Retrieves an {@link CMISRootResourcesCollection}, given its id.<br>
270     * @param id the identifier.
271     * @return the corresponding {@link CMISRootResourcesCollection}.
272     * @throws AmetysRepositoryException if an error occurs.
273     */
274    protected CMISRootResourcesCollection getCMISRootResourceCollection (String id) throws AmetysRepositoryException
275    {
276        try
277        {
278            Node node = getNode(id);
279            
280            if (!node.getPath().startsWith('/' + AmetysObjectResolver.ROOT_REPO))
281            {
282                throw new AmetysRepositoryException("Cannot resolve a Node outside Ametys tree");
283            }
284            
285            return getAmetysObject(node, null);
286        }
287        catch (RepositoryException e)
288        {
289            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
290        }
291    }
292
293    @Override
294    public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException
295    {
296        // l'id est de la forme <scheme>://uuid(/<cmis_id)
297        String uuid = id.substring(getScheme().length() + 3);
298        int index = uuid.indexOf("/");
299        
300        if (index != -1)
301        {
302            try
303            {
304                CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index));
305                Session session = root.getSession();
306                if (session == null)
307                {
308                    return false;
309                }
310                
311                ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
312                // refresh to make sure the object is not stall
313                session.getObject(cmisID).refresh();
314                return true;
315            }
316            catch (CmisObjectNotFoundException e)
317            {
318                return false;
319            }
320            catch (CmisBaseException e)
321            {
322                throw new AmetysRepositoryException("An error occurred while retriving CMIS object '" + id + "'.", e);
323            }
324        }
325        else
326        {
327            try
328            {
329                getNode(id);
330                return true;
331            }
332            catch (UnknownAmetysObjectException e)
333            {
334                return false;
335            }
336        }
337    }
338
339    public String getScheme()
340    {
341        return _scheme;
342    }
343
344    public Collection<String> getNodetypes()
345    {
346        return Collections.singletonList(_nodetype);
347    }
348
349    /**
350     * Returns the parent of the given {@link AmetysObject} .
351     * @param object a {@link AmetysObject}.
352     * @return the parent of the given {@link AmetysObject}.
353     * @throws AmetysRepositoryException if an error occurs.
354     */
355    public AmetysObject getParent(CMISRootResourcesCollection object) throws AmetysRepositoryException
356    {
357        try
358        {
359            Node node = object.getNode();
360            Node parentNode = node.getParent();
361        
362            return _resolver.resolve(parentNode, false);
363        }
364        catch (RepositoryException e)
365        {
366            throw new AmetysRepositoryException("Unable to retrieve parent object of object " + object.getName(), e);
367        }
368    }
369
370    /**
371     * Returns the JCR Node associated with the given object id.<br>
372     * This implementation assumes that the id is like <code>&lt;scheme&gt;://&lt;uuid&gt;</code>
373     * @param id the unique id of the object
374     * @return the JCR Node associated with the given id
375     */
376    protected Node getNode(String id)
377    {
378        // id = <scheme>://<uuid>
379        String uuid = id.substring(getScheme().length() + 3);
380        
381        javax.jcr.Session session = null;
382        try
383        {
384            session = _repository.login();
385            Node node = session.getNodeByIdentifier(uuid);
386            return node;
387        }
388        catch (ItemNotFoundException e)
389        {
390            if (session != null)
391            {
392                session.logout();
393            }
394
395            throw new UnknownAmetysObjectException("There's no node for id " + id, e);
396        }
397        catch (RepositoryException e)
398        {
399            if (session != null)
400            {
401                session.logout();
402            }
403
404            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
405        }
406    }
407    
408    /**
409     * Opening a Atom Pub Connection
410     * @param root the JCR root folder
411     * @return The created session or <code>null</code> if connection to CMIS server failed
412     */
413    public Session getAtomPubSession(CMISRootResourcesCollection root)
414    {
415        Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE);
416        String rootId = root.getId();
417        
418        return sessionCache.get(rootId, key -> _getAtomPubSession(root));
419    }
420
421    private Session _getAtomPubSession(CMISRootResourcesCollection root)
422    {
423        String url = root.getRepositoryUrl();
424        String user = root.getUser();
425        String password = root.getPassword();
426        String repositoryId = root.getRepositoryId();
427        
428        try
429        {
430            Map<String, String> params = new HashMap<>();
431
432            // user credentials
433            params.put(SessionParameter.USER, user);
434            params.put(SessionParameter.PASSWORD, password);
435
436            // connection settings
437            params.put(SessionParameter.ATOMPUB_URL, url);
438            params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
439            
440            params.put(SessionParameter.CONNECT_TIMEOUT, "5000");
441            params.put(SessionParameter.READ_TIMEOUT, "5000");
442            
443            if (StringUtils.isEmpty(repositoryId))
444            {
445                SessionFactory f = SessionFactoryImpl.newInstance();
446                List<org.apache.chemistry.opencmis.client.api.Repository> repositories = f.getRepositories(params);
447                repositoryId = repositories.listIterator().next().getId();
448                
449                // save repository id for next times
450                root.setRepositoryId(repositoryId);
451                root.saveChanges();
452            }
453            
454            params.put(SessionParameter.REPOSITORY_ID, repositoryId);
455            
456            // create session
457            SessionFactory f = SessionFactoryImpl.newInstance();
458            Session session = f.createSession(params);
459            return session;
460        }
461        catch (CmisConnectionException e)
462        {
463            getLogger().error("Connection to CMIS Atom Pub service ({}) failed", url, e);
464        }
465        catch (CmisObjectNotFoundException e)
466        {
467            getLogger().error("The CMIS Atom Pub service url ({}) refers to a non-existent repository ({})", url, repositoryId, e);
468        }
469        catch (CmisBaseException e)
470        {
471            // all others CMIS errors
472            getLogger().error("An error occured during call of CMIS Atom Pub service ({})", url, e);
473        }
474        
475        return null;
476    }
477    
478    public int getPriority()
479    {
480        return Observer.MAX_PRIORITY;
481    }
482    
483    public boolean supports(Event event)
484    {
485        String eventType = event.getId();
486        return ObservationConstants.EVENT_COLLECTION_DELETED.equals(eventType) || ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED.equals(eventType);
487    }
488    
489    public void observe(Event event, Map<String, Object> transientVars) throws Exception
490    {
491        Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE);
492        String rootId = (String) event.getArguments().get(ObservationConstants.ARGS_ID);
493        if (sessionCache.hasKey(rootId))
494        {
495            sessionCache.invalidate(rootId);
496        }
497    }
498    
499    /**
500     * Retrieves the extension point with available data types for {@link JCRResourcesCollection}
501     * @return the extension point with available data types for {@link JCRResourcesCollection}
502     */
503    public ModelItemTypeExtensionPoint getDataTypesExtensionPoint()
504    {
505        return _basicTypesExtensionPoint;
506    }
507}