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