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        // l'id est de la forme <scheme>://uuid(/<cmis_id)
198        String uuid = id.substring(getScheme().length() + 3);
199        int index = uuid.indexOf("/");
200        
201        if (index != -1)
202        {
203            CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index));
204            Session session = root.getSession();
205            if (session == null)
206            {
207                throw new UnknownAmetysObjectException("Connection to CMIS server failed");
208            }
209            
210            ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
211            CmisObject cmisObject = session.getObject(cmisID);
212            
213            BaseTypeId baseTypeId = cmisObject.getBaseTypeId();
214
215            if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER))
216            {
217                return new CMISResourcesCollection((Folder) cmisObject, root, null);
218            }
219            else if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT))
220            {
221                Document cmisDoc = (Document) cmisObject;
222                try
223                {
224                    // EXPLORER-243 alfresco's id point to a version, not to the real "live" document 
225                    if (!cmisDoc.isLatestVersion())
226                    {
227                        cmisDoc = cmisDoc.getObjectOfLatestVersion(false);
228                    }
229                }
230                catch (CmisBaseException e) 
231                {
232                    // EXPLORER-269 does nothing, nuxeo sometimes throws a CmisRuntimeException here
233                }
234                
235                return new CMISResource(cmisDoc, root, null);
236            }
237            else
238            {
239                throw new IllegalArgumentException("Unhandled CMIS type: " + baseTypeId);
240            }
241        }
242        else
243        {
244            return getCMISRootResourceCollection (id);
245        }
246    }
247    
248    @Override
249    public AmetysObject getAmetysObjectById(String id, javax.jcr.Session session) throws AmetysRepositoryException, RepositoryException
250    {
251        return getAmetysObjectById(id);
252    }
253    
254    /**
255     * Retrieves an {@link CMISRootResourcesCollection}, given its id.<br>
256     * @param id the identifier.
257     * @return the corresponding {@link CMISRootResourcesCollection}.
258     * @throws AmetysRepositoryException if an error occurs.
259     */
260    protected CMISRootResourcesCollection getCMISRootResourceCollection (String id) throws AmetysRepositoryException
261    {
262        try
263        {
264            Node node = getNode(id);
265            
266            if (!node.getPath().startsWith('/' + AmetysObjectResolver.ROOT_REPO))
267            {
268                throw new AmetysRepositoryException("Cannot resolve a Node outside Ametys tree");
269            }
270            
271            return getAmetysObject(node, null);
272        }
273        catch (RepositoryException e)
274        {
275            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
276        }
277    }
278
279    @Override
280    public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException
281    {            
282        // l'id est de la forme <scheme>://uuid(/<cmis_id)
283        String uuid = id.substring(getScheme().length() + 3);
284        int index = uuid.indexOf("/");
285        
286        if (index != -1)
287        {
288            CMISRootResourcesCollection root = getCMISRootResourceCollection (uuid.substring(0, index));
289            Session session = root.getSession();
290            if (session == null)
291            {
292                return false;
293            }
294            
295            ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
296            
297            return session.getObject(cmisID) == null;
298        }
299        else
300        {
301            try
302            {
303                getNode(id);
304                return true;
305            }
306            catch (UnknownAmetysObjectException e)
307            {
308                return false;
309            }
310        }
311    }
312
313    public String getScheme()
314    {
315        return _scheme;
316    }
317
318    public Collection<String> getNodetypes()
319    {
320        return Collections.singletonList(_nodetype);
321    }
322
323    /**
324     * Returns the parent of the given {@link AmetysObject} .
325     * @param object a {@link AmetysObject}.
326     * @return the parent of the given {@link AmetysObject}. 
327     * @throws AmetysRepositoryException if an error occurs.
328     */
329    public AmetysObject getParent(CMISRootResourcesCollection object) throws AmetysRepositoryException
330    {
331        try
332        {
333            Node node = object.getNode();
334            Node parentNode = node.getParent();
335        
336            return _resolver.resolve(parentNode, false);
337        }
338        catch (RepositoryException e)
339        {
340            throw new AmetysRepositoryException("Unable to retrieve parent object of object " + object.getName(), e);
341        }
342    }
343
344    /**
345     * Returns the JCR Node associated with the given object id.<br>
346     * This implementation assumes that the id is like <code>&lt;scheme&gt;://&lt;uuid&gt;</code>
347     * @param id the unique id of the object
348     * @return the JCR Node associated with the given id
349     */
350    protected Node getNode(String id)
351    {
352        // id = <scheme>://<uuid>
353        String uuid = id.substring(getScheme().length() + 3);
354        
355        javax.jcr.Session session = null;
356        try
357        {
358            session = _repository.login(); 
359            Node node = session.getNodeByIdentifier(uuid);
360            return node;
361        }
362        catch (ItemNotFoundException e)
363        {
364            if (session != null)
365            {
366                session.logout();
367            }
368
369            throw new UnknownAmetysObjectException("There's no node for id " + id, e);
370        }
371        catch (RepositoryException e)
372        {
373            if (session != null)
374            {
375                session.logout();
376            }
377
378            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
379        }
380    }
381    
382    /**
383     * Opening a Atom Pub Connection
384     * @param root the JCR root folder
385     * @return The created session or <code>null</code> if connection to CMIS server failed
386     */
387    public Session getAtomPubSession(CMISRootResourcesCollection root)
388    {
389        Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE);
390        String rootId = root.getId();
391        
392        return sessionCache.get(rootId, key -> _getAtomPubSession(root));
393    }
394
395    private Session _getAtomPubSession(CMISRootResourcesCollection root)
396    {
397        String url = root.getRepositoryUrl();
398        String user = root.getUser();
399        String password = root.getPassword();
400        String repositoryId = root.getRepositoryId();
401        
402        try
403        {
404            Map<String, String> params = new HashMap<>();
405
406            // user credentials
407            params.put(SessionParameter.USER, user);
408            params.put(SessionParameter.PASSWORD, password);
409
410            // connection settings
411            params.put(SessionParameter.ATOMPUB_URL, url);
412            params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
413            
414            if (StringUtils.isEmpty(repositoryId))
415            {
416                SessionFactory f = SessionFactoryImpl.newInstance();
417                List<org.apache.chemistry.opencmis.client.api.Repository> repositories = f.getRepositories(params);
418                repositoryId = repositories.listIterator().next().getId();
419                
420                // save repository id for next times
421                root.setRepositoryId(repositoryId);
422                root.saveChanges();
423            }
424            
425            params.put(SessionParameter.REPOSITORY_ID, repositoryId);
426            
427            // create session
428            SessionFactory f = SessionFactoryImpl.newInstance();
429            Session session = f.createSession(params);
430            return session;
431        }
432        catch (CmisConnectionException e)
433        {
434            getLogger().error("Connection to CMIS Atom Pub service ({}) failed", url, e);
435        }
436        catch (CmisObjectNotFoundException e)
437        {
438            getLogger().error("The CMIS Atom Pub service url ({}) refers to a non-existent repository ({})", url, repositoryId, e);
439        }
440        catch (CmisBaseException e)
441        {
442            // all others CMIS errors
443            getLogger().error("An error occured during call of CMIS Atom Pub service ({})", url, e);
444        }
445        
446        return null;
447    }
448    
449    public int getPriority(Event event)
450    {
451        return Observer.MAX_PRIORITY;
452    }
453    
454    public boolean supports(Event event)
455    {
456        String eventType = event.getId();
457        return ObservationConstants.EVENT_COLLECTION_DELETED.equals(eventType) || ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED.equals(eventType);
458    }
459    
460    public void observe(Event event, Map<String, Object> transientVars) throws Exception
461    {
462        Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE);
463        String rootId = (String) event.getArguments().get(ObservationConstants.ARGS_ID);
464        if (sessionCache.hasKey(rootId))
465        {
466            sessionCache.invalidate(rootId);
467        }
468    }
469    
470    /**
471     * Retrieves the extension point with available data types for {@link JCRResourcesCollection}
472     * @return the extension point with available data types for {@link JCRResourcesCollection}
473     */
474    public ModelItemTypeExtensionPoint getDataTypesExtensionPoint()
475    {
476        return _modelLessBasicTypesExtensionPoint;
477    }
478}