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; 050import org.apache.commons.lang3.Strings; 051 052import org.ametys.core.cache.AbstractCacheManager; 053import org.ametys.core.cache.Cache; 054import org.ametys.core.observation.Event; 055import org.ametys.core.observation.ObservationManager; 056import org.ametys.core.observation.Observer; 057import org.ametys.plugins.explorer.ObservationConstants; 058import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection; 059import org.ametys.plugins.repository.AmetysObject; 060import org.ametys.plugins.repository.AmetysObjectResolver; 061import org.ametys.plugins.repository.AmetysRepositoryException; 062import org.ametys.plugins.repository.RepositoryConstants; 063import org.ametys.plugins.repository.UnknownAmetysObjectException; 064import org.ametys.plugins.repository.data.type.ModelItemTypeExtensionPoint; 065import org.ametys.plugins.repository.jcr.JCRAmetysObjectFactory; 066import org.ametys.plugins.repository.provider.AbstractRepository; 067import org.ametys.runtime.i18n.I18nizableText; 068import org.ametys.runtime.plugin.component.AbstractLogEnabled; 069 070/** 071 * Create the Root of CMIS Resources Collections 072 */ 073public class CMISTreeFactory extends AbstractLogEnabled implements JCRAmetysObjectFactory<AmetysObject>, Configurable, Serviceable, Initializable, Observer 074{ 075 /** Nodetype for resources collection */ 076 public static final String CMIS_ROOT_COLLECTION_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":cmis-root-collection"; 077 078 private static final String __SESSION_CACHE = CMISTreeFactory.class.getName() + "$cmisSessionCache"; 079 080 /** The application {@link AmetysObjectResolver} */ 081 protected AmetysObjectResolver _resolver; 082 083 /** The configured scheme */ 084 protected String _scheme; 085 086 /** The configured nodetype */ 087 protected String _nodetype; 088 089 /** JCR Repository */ 090 protected Repository _repository; 091 092 private ObservationManager _observationManager; 093 094 private AbstractCacheManager _cacheManager; 095 096 private ModelItemTypeExtensionPoint _basicTypesExtensionPoint; 097 098 @Override 099 public void service(ServiceManager manager) throws ServiceException 100 { 101 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 102 _repository = (Repository) manager.lookup(AbstractRepository.ROLE); 103 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 104 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 105 _basicTypesExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_BASIC); 106 } 107 108 @Override 109 public void configure(Configuration configuration) throws ConfigurationException 110 { 111 _scheme = configuration.getChild("scheme").getValue(); 112 113 Configuration[] nodetypesConf = configuration.getChildren("nodetype"); 114 115 if (nodetypesConf.length != 1) 116 { 117 throw new ConfigurationException("A SimpleAmetysObjectFactory must have one and only one associated nodetype. " 118 + "The '" + configuration.getAttribute("id") + "' component has " + nodetypesConf.length); 119 } 120 121 _nodetype = nodetypesConf[0].getValue(); 122 } 123 124 public void initialize() throws Exception 125 { 126 _observationManager.registerObserver(this); 127 _cacheManager.createMemoryCache(__SESSION_CACHE, 128 new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_CACHE_CMIS_SESSION_LABEL"), 129 new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_CACHE_CMIS_SESSION_DESCRIPTION"), 130 true, 131 null); 132 } 133 134 @Override 135 public CMISRootResourcesCollection getAmetysObject(Node node, String parentPath) throws AmetysRepositoryException, RepositoryException 136 { 137 CMISRootResourcesCollection root = new CMISRootResourcesCollection(node, parentPath, this); 138 139 if (!root.hasValue(CMISRootResourcesCollection.DATA_REPOSITORY_URL)) 140 { 141 // Object just created, can't connect right now 142 return root; 143 } 144 145 try 146 { 147 Session session = getAtomPubSession(root); 148 149 Folder rootFolder = null; 150 if (session != null) 151 { 152 String mountPoint = root.getMountPoint(); 153 // mount point is the root folder 154 if (StringUtils.isBlank(mountPoint) || Strings.CS.equals(mountPoint, "/")) 155 { 156 rootFolder = session.getRootFolder(); 157 } 158 // any other valid mount point 159 else if (StringUtils.isNotBlank(mountPoint) && Strings.CS.startsWith(mountPoint, "/")) 160 { 161 try 162 { 163 rootFolder = (Folder) session.getObjectByPath(mountPoint); 164 } 165 catch (CmisObjectNotFoundException e) 166 { 167 getLogger().error("The mount point '{}' can't be found in the remote repository {}", mountPoint, root.getRepositoryId(), e); 168 } 169 } 170 171 // the mount point is valid 172 if (rootFolder != null) 173 { 174 root.connect(session, rootFolder); 175 } 176 } 177 } 178 catch (CmisConnectionException e) 179 { 180 getLogger().error("Connection to CMIS Atom Pub service failed", e); 181 } 182 catch (CmisObjectNotFoundException e) 183 { 184 getLogger().error("The CMIS Atom Pub service url refers to a non-existent repository", e); 185 } 186 catch (CmisBaseException e) 187 { 188 // all others CMIS errors 189 getLogger().error("An error occured during call of CMIS Atom Pub service", e); 190 } 191 192 return root; 193 } 194 195 @Override 196 public AmetysObject getAmetysObjectById(String id) throws AmetysRepositoryException 197 { 198 try 199 { 200 // l'id est de la forme <scheme>://uuid(/<cmis_id) 201 String uuid = id.substring(getScheme().length() + 3); 202 int index = uuid.indexOf("/"); 203 204 if (index != -1) 205 { 206 CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index)); 207 Session session = root.getSession(); 208 if (session == null) 209 { 210 throw new UnknownAmetysObjectException("Connection to CMIS server failed"); 211 } 212 213 ObjectId cmisID = session.createObjectId(uuid.substring(index + 1)); 214 CmisObject cmisObject = session.getObject(cmisID); 215 // make sure the object is not stall when resolving 216 cmisObject.refresh(); 217 218 BaseTypeId baseTypeId = cmisObject.getBaseTypeId(); 219 220 if (baseTypeId.equals(BaseTypeId.CMIS_FOLDER)) 221 { 222 return new CMISResourcesCollection((Folder) cmisObject, root, null); 223 } 224 else if (baseTypeId.equals(BaseTypeId.CMIS_DOCUMENT)) 225 { 226 Document cmisDoc = (Document) cmisObject; 227 try 228 { 229 // EXPLORER-243 alfresco's id point to a version, not to the real "live" document 230 if (!cmisDoc.isLatestVersion()) 231 { 232 cmisDoc = cmisDoc.getObjectOfLatestVersion(false); 233 } 234 } 235 catch (CmisBaseException e) 236 { 237 // EXPLORER-269 does nothing, nuxeo sometimes throws a CmisRuntimeException here 238 } 239 240 return new CMISResource(cmisDoc, root, null); 241 } 242 else 243 { 244 throw new IllegalArgumentException("Unhandled CMIS type: " + baseTypeId); 245 } 246 } 247 else 248 { 249 return getCMISRootResourceCollection (id); 250 } 251 } 252 catch (CmisObjectNotFoundException e) 253 { 254 throw new UnknownAmetysObjectException("No CMIS object found with id " + id, e); 255 } 256 catch (CmisBaseException e) 257 { 258 throw new AmetysRepositoryException("An error occurred while retriving CMIS object '" + id + "'.", e); 259 } 260 } 261 262 @Override 263 public AmetysObject getAmetysObjectById(String id, javax.jcr.Session session) throws AmetysRepositoryException, RepositoryException 264 { 265 return getAmetysObjectById(id); 266 } 267 268 /** 269 * Retrieves an {@link CMISRootResourcesCollection}, given its id.<br> 270 * @param id the identifier. 271 * @return the corresponding {@link CMISRootResourcesCollection}. 272 * @throws AmetysRepositoryException if an error occurs. 273 */ 274 protected CMISRootResourcesCollection getCMISRootResourceCollection (String id) throws AmetysRepositoryException 275 { 276 try 277 { 278 Node node = getNode(id); 279 280 if (!node.getPath().startsWith('/' + AmetysObjectResolver.ROOT_REPO)) 281 { 282 throw new AmetysRepositoryException("Cannot resolve a Node outside Ametys tree"); 283 } 284 285 return getAmetysObject(node, null); 286 } 287 catch (RepositoryException e) 288 { 289 throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e); 290 } 291 } 292 293 @Override 294 public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException 295 { 296 // l'id est de la forme <scheme>://uuid(/<cmis_id) 297 String uuid = id.substring(getScheme().length() + 3); 298 int index = uuid.indexOf("/"); 299 300 if (index != -1) 301 { 302 try 303 { 304 CMISRootResourcesCollection root = getCMISRootResourceCollection (getScheme() + "://" + uuid.substring(0, index)); 305 Session session = root.getSession(); 306 if (session == null) 307 { 308 return false; 309 } 310 311 ObjectId cmisID = session.createObjectId(uuid.substring(index + 1)); 312 // refresh to make sure the object is not stall 313 session.getObject(cmisID).refresh(); 314 return true; 315 } 316 catch (CmisObjectNotFoundException e) 317 { 318 return false; 319 } 320 catch (CmisBaseException e) 321 { 322 throw new AmetysRepositoryException("An error occurred while retriving CMIS object '" + id + "'.", e); 323 } 324 } 325 else 326 { 327 try 328 { 329 getNode(id); 330 return true; 331 } 332 catch (UnknownAmetysObjectException e) 333 { 334 return false; 335 } 336 } 337 } 338 339 public String getScheme() 340 { 341 return _scheme; 342 } 343 344 public Collection<String> getNodetypes() 345 { 346 return Collections.singletonList(_nodetype); 347 } 348 349 /** 350 * Returns the parent of the given {@link AmetysObject} . 351 * @param object a {@link AmetysObject}. 352 * @return the parent of the given {@link AmetysObject}. 353 * @throws AmetysRepositoryException if an error occurs. 354 */ 355 public AmetysObject getParent(CMISRootResourcesCollection object) throws AmetysRepositoryException 356 { 357 try 358 { 359 Node node = object.getNode(); 360 Node parentNode = node.getParent(); 361 362 return _resolver.resolve(parentNode, false); 363 } 364 catch (RepositoryException e) 365 { 366 throw new AmetysRepositoryException("Unable to retrieve parent object of object " + object.getName(), e); 367 } 368 } 369 370 /** 371 * Returns the JCR Node associated with the given object id.<br> 372 * This implementation assumes that the id is like <code><scheme>://<uuid></code> 373 * @param id the unique id of the object 374 * @return the JCR Node associated with the given id 375 */ 376 protected Node getNode(String id) 377 { 378 // id = <scheme>://<uuid> 379 String uuid = id.substring(getScheme().length() + 3); 380 381 javax.jcr.Session session = null; 382 try 383 { 384 session = _repository.login(); 385 Node node = session.getNodeByIdentifier(uuid); 386 return node; 387 } 388 catch (ItemNotFoundException e) 389 { 390 if (session != null) 391 { 392 session.logout(); 393 } 394 395 throw new UnknownAmetysObjectException("There's no node for id " + id, e); 396 } 397 catch (RepositoryException e) 398 { 399 if (session != null) 400 { 401 session.logout(); 402 } 403 404 throw new AmetysRepositoryException("Unable to get AmetysObject for id: " + id, e); 405 } 406 } 407 408 /** 409 * Opening a Atom Pub Connection 410 * @param root the JCR root folder 411 * @return The created session or <code>null</code> if connection to CMIS server failed 412 */ 413 public Session getAtomPubSession(CMISRootResourcesCollection root) 414 { 415 Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE); 416 String rootId = root.getId(); 417 418 return sessionCache.get(rootId, key -> _getAtomPubSession(root)); 419 } 420 421 private Session _getAtomPubSession(CMISRootResourcesCollection root) 422 { 423 String url = root.getRepositoryUrl(); 424 String user = root.getUser(); 425 String password = root.getPassword(); 426 String repositoryId = root.getRepositoryId(); 427 428 try 429 { 430 Map<String, String> params = new HashMap<>(); 431 432 // user credentials 433 params.put(SessionParameter.USER, user); 434 params.put(SessionParameter.PASSWORD, password); 435 436 // connection settings 437 params.put(SessionParameter.ATOMPUB_URL, url); 438 params.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value()); 439 440 params.put(SessionParameter.CONNECT_TIMEOUT, "5000"); 441 params.put(SessionParameter.READ_TIMEOUT, "5000"); 442 443 if (StringUtils.isEmpty(repositoryId)) 444 { 445 SessionFactory f = SessionFactoryImpl.newInstance(); 446 List<org.apache.chemistry.opencmis.client.api.Repository> repositories = f.getRepositories(params); 447 repositoryId = repositories.listIterator().next().getId(); 448 449 // save repository id for next times 450 root.setRepositoryId(repositoryId); 451 root.saveChanges(); 452 } 453 454 params.put(SessionParameter.REPOSITORY_ID, repositoryId); 455 456 // create session 457 SessionFactory f = SessionFactoryImpl.newInstance(); 458 Session session = f.createSession(params); 459 return session; 460 } 461 catch (CmisConnectionException e) 462 { 463 getLogger().error("Connection to CMIS Atom Pub service ({}) failed", url, e); 464 } 465 catch (CmisObjectNotFoundException e) 466 { 467 getLogger().error("The CMIS Atom Pub service url ({}) refers to a non-existent repository ({})", url, repositoryId, e); 468 } 469 catch (CmisBaseException e) 470 { 471 // all others CMIS errors 472 getLogger().error("An error occured during call of CMIS Atom Pub service ({})", url, e); 473 } 474 475 return null; 476 } 477 478 public int getPriority() 479 { 480 return Observer.MAX_PRIORITY; 481 } 482 483 public boolean supports(Event event) 484 { 485 String eventType = event.getId(); 486 return ObservationConstants.EVENT_COLLECTION_DELETED.equals(eventType) || ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED.equals(eventType); 487 } 488 489 public void observe(Event event, Map<String, Object> transientVars) throws Exception 490 { 491 Cache<String, Session> sessionCache = _cacheManager.get(__SESSION_CACHE); 492 String rootId = (String) event.getArguments().get(ObservationConstants.ARGS_ID); 493 if (sessionCache.hasKey(rootId)) 494 { 495 sessionCache.invalidate(rootId); 496 } 497 } 498 499 /** 500 * Retrieves the extension point with available data types for {@link JCRResourcesCollection} 501 * @return the extension point with available data types for {@link JCRResourcesCollection} 502 */ 503 public ModelItemTypeExtensionPoint getDataTypesExtensionPoint() 504 { 505 return _basicTypesExtensionPoint; 506 } 507}