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