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.lang.StringUtils;
050
051import org.ametys.core.observation.Event;
052import org.ametys.core.observation.ObservationManager;
053import org.ametys.core.observation.Observer;
054import org.ametys.plugins.explorer.ObservationConstants;
055import org.ametys.plugins.repository.AmetysObject;
056import org.ametys.plugins.repository.AmetysObjectResolver;
057import org.ametys.plugins.repository.AmetysRepositoryException;
058import org.ametys.plugins.repository.RepositoryConstants;
059import org.ametys.plugins.repository.UnknownAmetysObjectException;
060import org.ametys.plugins.repository.jcr.JCRAmetysObjectFactory;
061import org.ametys.plugins.repository.provider.AbstractRepository;
062import org.ametys.runtime.plugin.component.AbstractLogEnabled;
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                root.connect(session, rootFolder);
137            }
138        }
139        catch (CmisConnectionException e)
140        {
141            getLogger().error("Connection to CMIS Atom Pub service failed", e);
142        }
143        catch (CmisObjectNotFoundException e)
144        {
145            getLogger().error("The CMIS Atom Pub service url refers to a non-existent repository", e);
146        }
147        catch (CmisBaseException e)
148        {
149            // all others CMIS errors
150            getLogger().error("An error occured during call of CMIS Atom Pub service", e);
151        }
152        
153        return root;
154    }
155
156    @Override
157    public AmetysObject getAmetysObjectById(String id) throws AmetysRepositoryException
158    {
159        // l'id est de la forme <scheme>://uuid(/<cmis_id)
160        String uuid = id.substring(getScheme().length() + 3);
161        int index = uuid.indexOf("/");
162        
163        if (index != -1)
164        {
165            CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index));
166            Session session = root.getSession();
167            if (session == null)
168            {
169                throw new UnknownAmetysObjectException("Connection to CMIS server failed");
170            }
171            
172            ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
173            CmisObject cmisObject = session.getObject(cmisID);
174            
175            BaseTypeId baseTypeId = cmisObject.getBaseTypeId();
176
177            if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER))
178            {
179                return new CMISResourcesCollection((Folder) cmisObject, root, null);
180            }
181            else if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT))
182            {
183                Document cmisDoc = (Document) cmisObject;
184                try
185                {
186                    // EXPLORER-243 alfresco's id point to a version, not to the real "live" document 
187                    if (!cmisDoc.isLatestVersion())
188                    {
189                        cmisDoc = cmisDoc.getObjectOfLatestVersion(false);
190                    }
191                }
192                catch (CmisBaseException e) 
193                {
194                    // EXPLORER-269 does nothing, nuxeo sometimes throws a CmisRuntimeException here
195                }
196                
197                return new CMISResource(cmisDoc, root, null);
198            }
199            else
200            {
201                throw new IllegalArgumentException("Unhandled CMIS type: " + baseTypeId);
202            }
203        }
204        else
205        {
206            return getCMISRootResourceCollection (id);
207        }
208    }
209    
210    @Override
211    public AmetysObject getAmetysObjectById(String id, javax.jcr.Session session) throws AmetysRepositoryException, RepositoryException
212    {
213        return getAmetysObjectById(id);
214    }
215    
216    /**
217     * Retrieves an {@link CMISRootResourcesCollection}, given its id.<br>
218     * @param id the identifier.
219     * @return the corresponding {@link CMISRootResourcesCollection}.
220     * @throws AmetysRepositoryException if an error occurs.
221     */
222    protected CMISRootResourcesCollection getCMISRootResourceCollection (String id) throws AmetysRepositoryException
223    {
224        try
225        {
226            Node node = getNode(id);
227            
228            if (!node.getPath().startsWith('/' + AmetysObjectResolver.ROOT_REPO))
229            {
230                throw new AmetysRepositoryException("Cannot resolve a Node outside Ametys tree");
231            }
232            
233            return getAmetysObject(node, null);
234        }
235        catch (RepositoryException e)
236        {
237            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
238        }
239    }
240
241    @Override
242    public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException
243    {            
244        // l'id est de la forme <scheme>://uuid(/<cmis_id)
245        String uuid = id.substring(getScheme().length() + 3);
246        int index = uuid.indexOf("/");
247        
248        if (index != -1)
249        {
250            CMISRootResourcesCollection root = getCMISRootResourceCollection (uuid.substring(0, index));
251            Session session = root.getSession();
252            if (session == null)
253            {
254                return false;
255            }
256            
257            ObjectId cmisID = session.createObjectId(uuid.substring(index + 1));
258            
259            return session.getObject(cmisID) == null;
260        }
261        else
262        {
263            try
264            {
265                getNode(id);
266                return true;
267            }
268            catch (UnknownAmetysObjectException e)
269            {
270                return false;
271            }
272        }
273    }
274
275    public String getScheme()
276    {
277        return _scheme;
278    }
279
280    public Collection<String> getNodetypes()
281    {
282        return Collections.singletonList(_nodetype);
283    }
284
285    /**
286     * Returns the parent of the given {@link AmetysObject} .
287     * @param object a {@link AmetysObject}.
288     * @return the parent of the given {@link AmetysObject}. 
289     * @throws AmetysRepositoryException if an error occurs.
290     */
291    public AmetysObject getParent(CMISRootResourcesCollection object) throws AmetysRepositoryException
292    {
293        try
294        {
295            Node node = object.getNode();
296            Node parentNode = node.getParent();
297        
298            return _resolver.resolve(parentNode, false);
299        }
300        catch (RepositoryException e)
301        {
302            throw new AmetysRepositoryException("Unable to retrieve parent object of object " + object.getName(), e);
303        }
304    }
305
306    /**
307     * Returns the JCR Node associated with the given object id.<br>
308     * This implementation assumes that the id is like <code>&lt;scheme&gt;://&lt;uuid&gt;</code>
309     * @param id the unique id of the object
310     * @return the JCR Node associated with the given id
311     */
312    protected Node getNode(String id)
313    {
314        // id = <scheme>://<uuid>
315        String uuid = id.substring(getScheme().length() + 3);
316        
317        javax.jcr.Session session = null;
318        try
319        {
320            session = _repository.login(); 
321            Node node = session.getNodeByIdentifier(uuid);
322            return node;
323        }
324        catch (ItemNotFoundException e)
325        {
326            if (session != null)
327            {
328                session.logout();
329            }
330
331            throw new UnknownAmetysObjectException("There's no node for id " + id, e);
332        }
333        catch (RepositoryException e)
334        {
335            if (session != null)
336            {
337                session.logout();
338            }
339
340            throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e);
341        }
342    }
343    
344    /**
345     * Opening a Atom Pub Connection
346     * @param root the JCR root folder
347     * @return The created session or <code>null</code> if connection to CMIS server failed
348     */
349    public Session getAtomPubSession(CMISRootResourcesCollection root)
350    {
351        String rootId = root.getId();
352        if (_sessionCache.containsKey(rootId))
353        {
354            return _sessionCache.get(rootId);
355        }
356
357        String url = root.getRepositoryUrl();
358        String user = root.getUser();
359        String password = root.getPassword();
360        String repositoryId = root.getRepositoryId();
361        
362        try
363        {
364            Map<String, String> params = new HashMap<>();
365
366            // user credentials
367            params.put(SessionParameter.USER, user);
368            params.put(SessionParameter.PASSWORD, password);
369
370            // connection settings
371            params.put(SessionParameter.ATOMPUB_URL, url);
372            params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());
373            
374            if (StringUtils.isEmpty(repositoryId))
375            {
376                SessionFactory f = SessionFactoryImpl.newInstance();
377                List<org.apache.chemistry.opencmis.client.api.Repository> repositories = f.getRepositories(params);
378                repositoryId = repositories.listIterator().next().getId();
379                
380                // save repository id for next times
381                root.setRepositoryId(repositoryId);
382                root.saveChanges();
383            }
384            
385            params.put(SessionParameter.REPOSITORY_ID, repositoryId);
386            
387            // create session
388            SessionFactory f = SessionFactoryImpl.newInstance();
389            Session session = f.createSession(params);
390            _sessionCache.put(rootId, session);
391            return session;
392        }
393        catch (CmisConnectionException e)
394        {
395            getLogger().error("Connection to CMIS Atom Pub service ({}) failed", url, e);
396        }
397        catch (CmisObjectNotFoundException e)
398        {
399            getLogger().error("The CMIS Atom Pub service url ({}) refers to a non-existent repository ({})", url, repositoryId, e);
400        }
401        catch (CmisBaseException e)
402        {
403            // all others CMIS errors
404            getLogger().error("An error occured during call of CMIS Atom Pub service ({})", url, e);
405        }
406        
407        return null;
408    }
409    
410    public int getPriority(Event event)
411    {
412        return Observer.MAX_PRIORITY;
413    }
414    
415    public boolean supports(Event event)
416    {
417        String eventType = event.getId();
418        return ObservationConstants.EVENT_COLLECTION_DELETED.equals(eventType) || ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED.equals(eventType);
419    }
420    
421    public void observe(Event event, Map<String, Object> transientVars) throws Exception
422    {
423        String rootId = (String) event.getArguments().get(ObservationConstants.ARGS_ID);
424        if (_sessionCache.containsKey(rootId))
425        {
426            _sessionCache.remove(rootId);
427        }
428    }
429}