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><scheme>://<uuid></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}