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