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