001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.cmis; 029 030import static org.opencms.cmis.CmsCmisUtil.addAction; 031import static org.opencms.cmis.CmsCmisUtil.addPropertyDateTime; 032import static org.opencms.cmis.CmsCmisUtil.addPropertyId; 033import static org.opencms.cmis.CmsCmisUtil.addPropertyString; 034import static org.opencms.cmis.CmsCmisUtil.handleCmsException; 035import static org.opencms.cmis.CmsCmisUtil.millisToCalendar; 036 037import org.opencms.file.CmsObject; 038import org.opencms.file.CmsResource; 039import org.opencms.file.CmsResourceFilter; 040import org.opencms.file.CmsUser; 041import org.opencms.lock.CmsLock; 042import org.opencms.main.CmsException; 043import org.opencms.relations.CmsRelation; 044import org.opencms.relations.CmsRelationFilter; 045import org.opencms.relations.CmsRelationType; 046import org.opencms.security.CmsPermissionSet; 047import org.opencms.util.CmsUUID; 048 049import java.util.ArrayList; 050import java.util.GregorianCalendar; 051import java.util.LinkedHashSet; 052import java.util.List; 053import java.util.Set; 054import java.util.regex.Matcher; 055import java.util.regex.Pattern; 056 057import org.apache.chemistry.opencmis.commons.PropertyIds; 058import org.apache.chemistry.opencmis.commons.data.Ace; 059import org.apache.chemistry.opencmis.commons.data.Acl; 060import org.apache.chemistry.opencmis.commons.data.AllowableActions; 061import org.apache.chemistry.opencmis.commons.data.ObjectData; 062import org.apache.chemistry.opencmis.commons.data.Properties; 063import org.apache.chemistry.opencmis.commons.enums.Action; 064import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; 065import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships; 066import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException; 067import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException; 068import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; 069import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl; 070import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl; 071import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl; 072import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl; 073import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl; 074 075/** 076 * Helper class for CMIS CRUD operations on relation objects.<p> 077 * 078 * Since CMIS requires any object to have an ID by which it is accessed, but OpenCms relations 079 * are not addressable by ids, we invent an artificial relation id string of the form 080 * REL_(SOURCE_ID)_(TARGET_ID)_(TYPE).<p> 081 * 082 */ 083public class CmsCmisRelationHelper implements I_CmsCmisObjectHelper { 084 085 /** 086 * A class which contains the necessary information to identify a relation object.<p> 087 */ 088 public static class RelationKey { 089 090 /** The internal OpenCms relation object (optional). */ 091 private CmsRelation m_relation; 092 093 /** The relation type string. */ 094 private String m_relType; 095 096 /** The internal OpenCms resource which is the relation source (optional). */ 097 private CmsResource m_source; 098 099 /** The source id of the relation. */ 100 private CmsUUID m_sourceId; 101 102 /** The target id of the relation. */ 103 private CmsUUID m_targetId; 104 105 /** 106 * Creates a new relation key.<p> 107 * 108 * @param sourceId the source id 109 * @param targetId the target id 110 * @param relType the relation type 111 */ 112 public RelationKey(CmsUUID sourceId, CmsUUID targetId, String relType) { 113 114 m_sourceId = sourceId; 115 m_targetId = targetId; 116 m_relType = relType; 117 } 118 119 /** 120 * Reads the actual resource and relation data from the OpenCms VFS.<p> 121 * 122 * @param cms the CMS context to use for reading the data 123 */ 124 public void fillRelation(CmsObject cms) { 125 126 try { 127 m_source = cms.readResource(m_sourceId); 128 List<CmsRelation> relations = cms.getRelationsForResource( 129 m_source, 130 CmsRelationFilter.TARGETS.filterStructureId(m_targetId).filterType(getRelationType(m_relType))); 131 if (relations.isEmpty()) { 132 throw new CmisObjectNotFoundException(toString()); 133 } 134 m_relation = relations.get(0); 135 } catch (CmsException e) { 136 CmsCmisUtil.handleCmsException(e); 137 } 138 } 139 140 /** 141 * Gets the relation object.<p> 142 * 143 * @return the relation object 144 */ 145 public CmsRelation getRelation() { 146 147 return m_relation; 148 } 149 150 /** 151 * Gets the relation type.<p> 152 * 153 * @return the relation type 154 */ 155 public String getRelType() { 156 157 return m_relType; 158 } 159 160 /** 161 * Gets the source resource of the relation.<p> 162 * 163 * @return the source of the relation 164 */ 165 public CmsResource getSource() { 166 167 return m_source; 168 } 169 170 /** 171 * Gets the source id.<p> 172 * 173 * @return the source id 174 */ 175 public CmsUUID getSourceId() { 176 177 return m_sourceId; 178 } 179 180 /** 181 * Gets the target id of the relation.<p> 182 * 183 * @return the target id 184 */ 185 public CmsUUID getTargetId() { 186 187 return m_targetId; 188 } 189 190 /** 191 * Sets the relation type.<p> 192 * 193 * @param relType the relation type 194 */ 195 public void setRelType(String relType) { 196 197 m_relType = relType; 198 } 199 200 /** 201 * Sets the source id.<p> 202 * 203 * @param sourceId the source id 204 */ 205 public void setSourceId(CmsUUID sourceId) { 206 207 m_sourceId = sourceId; 208 } 209 210 /** 211 * Sets the target id.<p> 212 * 213 * @param targetId the target id 214 */ 215 public void setTargetId(CmsUUID targetId) { 216 217 m_targetId = targetId; 218 } 219 220 /** 221 * @see java.lang.Object#toString() 222 */ 223 @Override 224 public String toString() { 225 226 return createKey(m_sourceId, m_targetId, m_relType); 227 } 228 } 229 230 /** The prefix used to identify relation ids. */ 231 public static final String RELATION_ID_PREFIX = "REL_"; 232 233 /** The pattern which relation ids should match. */ 234 public static final Pattern RELATION_PATTERN = Pattern.compile("^REL_(" 235 + CmsUUID.UUID_REGEX 236 + ")_(" 237 + CmsUUID.UUID_REGEX 238 + ")_(.*)$"); 239 240 /** The underlying CMIS repository. */ 241 private CmsCmisRepository m_repository; 242 243 /** 244 * Creates a new relation helper for the given repository.<p> 245 * 246 * @param repository the repository 247 */ 248 public CmsCmisRelationHelper(CmsCmisRepository repository) { 249 250 m_repository = repository; 251 } 252 253 /** 254 * Creates a relation id string from the source and target ids and a relation type.<p> 255 * 256 * @param source the source id 257 * @param target the target id 258 * @param relType the relation type 259 * 260 * @return the relation id 261 */ 262 protected static String createKey(CmsUUID source, CmsUUID target, String relType) { 263 264 return RELATION_ID_PREFIX + source + "_" + target + "_" + relType; 265 } 266 267 /** 268 * Gets a relation type by name.<p> 269 * 270 * @param typeName the relation type name 271 * 272 * @return the relation type with the matching name 273 */ 274 protected static CmsRelationType getRelationType(String typeName) { 275 276 for (CmsRelationType relType : CmsRelationType.getAll()) { 277 if (relType.getName().equalsIgnoreCase(typeName)) { 278 return relType; 279 } 280 } 281 return null; 282 } 283 284 /** 285 * @see org.opencms.cmis.I_CmsCmisObjectHelper#deleteObject(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean) 286 */ 287 public void deleteObject(CmsCmisCallContext context, String objectId, boolean allVersions) { 288 289 try { 290 291 RelationKey rk = parseRelationKey(objectId); 292 CmsUUID sourceId = rk.getSourceId(); 293 CmsObject cms = m_repository.getCmsObject(context); 294 CmsResource sourceResource = cms.readResource(sourceId); 295 boolean wasLocked = CmsCmisUtil.ensureLock(cms, sourceResource); 296 try { 297 CmsRelationFilter relFilter = CmsRelationFilter.ALL.filterType(getRelationType(rk.getRelType())).filterStructureId( 298 rk.getTargetId()); 299 cms.deleteRelationsFromResource(sourceResource.getRootPath(), relFilter); 300 } finally { 301 if (wasLocked) { 302 cms.unlockResource(sourceResource); 303 } 304 } 305 } catch (CmsException e) { 306 CmsCmisUtil.handleCmsException(e); 307 } 308 } 309 310 /** 311 * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAcl(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean) 312 */ 313 public Acl getAcl(CmsCmisCallContext context, String objectId, boolean onlyBasicPermissions) { 314 315 CmsObject cms = m_repository.getCmsObject(context); 316 RelationKey rk = parseRelationKey(objectId); 317 rk.fillRelation(cms); 318 return collectAcl(cms, rk.getSource(), onlyBasicPermissions); 319 } 320 321 /** 322 * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAllowableActions(org.opencms.cmis.CmsCmisCallContext, java.lang.String) 323 */ 324 public AllowableActions getAllowableActions(CmsCmisCallContext context, String objectId) { 325 326 CmsObject cms = m_repository.getCmsObject(context); 327 RelationKey rk = parseRelationKey(objectId); 328 rk.fillRelation(cms); 329 return collectAllowableActions(cms, rk.getSource(), rk.getRelation()); 330 } 331 332 /** 333 * @see org.opencms.cmis.I_CmsCmisObjectHelper#getObject(org.opencms.cmis.CmsCmisCallContext, java.lang.String, java.lang.String, boolean, org.apache.chemistry.opencmis.commons.enums.IncludeRelationships, java.lang.String, boolean, boolean) 334 */ 335 public ObjectData getObject( 336 CmsCmisCallContext context, 337 String objectId, 338 String filter, 339 boolean includeAllowableActions, 340 IncludeRelationships includeRelationships, 341 String renditionFilter, 342 boolean includePolicyIds, 343 boolean includeAcl) { 344 345 CmsObject cms = m_repository.getCmsObject(context); 346 RelationKey rk = parseRelationKey(objectId); 347 rk.fillRelation(cms); 348 Set<String> filterSet = CmsCmisUtil.splitFilter(filter); 349 ObjectData result = collectObjectData( 350 context, 351 cms, 352 rk.getSource(), 353 rk.getRelation(), 354 filterSet, 355 includeAllowableActions, 356 includeAcl); 357 return result; 358 } 359 360 /** 361 * Compiles the ACL for a relation.<p> 362 * 363 * @param cms the CMS context 364 * @param resource the resource for which to collect the ACLs 365 * @param onlyBasic flag to only include basic ACEs 366 * 367 * @return the ACL for the resource 368 */ 369 protected Acl collectAcl(CmsObject cms, CmsResource resource, boolean onlyBasic) { 370 371 AccessControlListImpl cmisAcl = new AccessControlListImpl(); 372 List<Ace> cmisAces = new ArrayList<Ace>(); 373 cmisAcl.setAces(cmisAces); 374 cmisAcl.setExact(Boolean.FALSE); 375 return cmisAcl; 376 } 377 378 /** 379 * Collects the allowable actions for a relation.<p> 380 * 381 * @param cms the current CMS context 382 * @param file the source of the relation 383 * @param relation the relation object 384 * 385 * @return the allowable actions for the given resource 386 */ 387 protected AllowableActions collectAllowableActions(CmsObject cms, CmsResource file, CmsRelation relation) { 388 389 try { 390 Set<Action> aas = new LinkedHashSet<Action>(); 391 AllowableActionsImpl result = new AllowableActionsImpl(); 392 393 CmsLock lock = cms.getLock(file); 394 CmsUser user = cms.getRequestContext().getCurrentUser(); 395 boolean canWrite = !cms.getRequestContext().getCurrentProject().isOnlineProject() 396 && (lock.isOwnedBy(user) || lock.isLockableBy(user)) 397 && cms.hasPermissions(file, CmsPermissionSet.ACCESS_WRITE, false, CmsResourceFilter.DEFAULT); 398 addAction(aas, Action.CAN_GET_PROPERTIES, true); 399 addAction(aas, Action.CAN_DELETE_OBJECT, canWrite && !relation.getType().isDefinedInContent()); 400 result.setAllowableActions(aas); 401 return result; 402 } catch (CmsException e) { 403 handleCmsException(e); 404 return null; 405 } 406 } 407 408 /** 409 * Fills in an ObjectData record.<p> 410 * 411 * @param context the call context 412 * @param cms the CMS context 413 * @param resource the resource for which we want the ObjectData 414 * @param relation the relation object 415 * @param filter the property filter string 416 * @param includeAllowableActions true if the allowable actions should be included 417 * @param includeAcl true if the ACL entries should be included 418 * 419 * @return the object data 420 */ 421 protected ObjectData collectObjectData( 422 CmsCmisCallContext context, 423 CmsObject cms, 424 CmsResource resource, 425 CmsRelation relation, 426 Set<String> filter, 427 boolean includeAllowableActions, 428 boolean includeAcl) { 429 430 ObjectDataImpl result = new ObjectDataImpl(); 431 ObjectInfoImpl objectInfo = new ObjectInfoImpl(); 432 433 result.setProperties(collectProperties(cms, resource, relation, filter, objectInfo)); 434 435 if (includeAllowableActions) { 436 result.setAllowableActions(collectAllowableActions(cms, resource, relation)); 437 } 438 439 if (includeAcl) { 440 result.setAcl(collectAcl(cms, resource, true)); 441 result.setIsExactAcl(Boolean.FALSE); 442 } 443 444 if (context.isObjectInfoRequired()) { 445 objectInfo.setObject(result); 446 context.getObjectInfoHandler().addObjectInfo(objectInfo); 447 } 448 return result; 449 } 450 451 /** 452 * Gathers all base properties of a file or folder. 453 * 454 * @param cms the current CMS context 455 * @param resource the file for which we want the properties 456 * @param relation the relation object 457 * @param orgfilter the property filter 458 * @param objectInfo the object info handler 459 * 460 * @return the properties for the given resource 461 */ 462 protected Properties collectProperties( 463 CmsObject cms, 464 CmsResource resource, 465 CmsRelation relation, 466 Set<String> orgfilter, 467 ObjectInfoImpl objectInfo) { 468 469 CmsCmisTypeManager tm = m_repository.getTypeManager(); 470 471 if (resource == null) { 472 throw new IllegalArgumentException("Resource may not be null."); 473 } 474 475 // copy filter 476 Set<String> filter = (orgfilter == null ? null : new LinkedHashSet<String>(orgfilter)); 477 478 // find base type 479 String typeId = "opencms:" + relation.getType().getName(); 480 objectInfo.setBaseType(BaseTypeId.CMIS_RELATIONSHIP); 481 objectInfo.setTypeId(typeId); 482 objectInfo.setContentType(null); 483 objectInfo.setFileName(null); 484 objectInfo.setHasAcl(false); 485 objectInfo.setHasContent(false); 486 objectInfo.setVersionSeriesId(null); 487 objectInfo.setIsCurrentVersion(true); 488 objectInfo.setRelationshipSourceIds(null); 489 objectInfo.setRelationshipTargetIds(null); 490 objectInfo.setRenditionInfos(null); 491 objectInfo.setSupportsDescendants(false); 492 objectInfo.setSupportsFolderTree(false); 493 objectInfo.setSupportsPolicies(false); 494 objectInfo.setSupportsRelationships(false); 495 objectInfo.setWorkingCopyId(null); 496 objectInfo.setWorkingCopyOriginalId(null); 497 498 // let's do it 499 try { 500 PropertiesImpl result = new PropertiesImpl(); 501 502 // id 503 String id = createKey(relation); 504 addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_ID, id); 505 objectInfo.setId(id); 506 507 // name 508 String name = createReadableName(relation); 509 addPropertyString(tm, result, typeId, filter, PropertyIds.NAME, name); 510 objectInfo.setName(name); 511 512 // created and modified by 513 CmsUUID creatorId = resource.getUserCreated(); 514 CmsUUID modifierId = resource.getUserLastModified(); 515 String creatorName = creatorId.toString(); 516 String modifierName = modifierId.toString(); 517 try { 518 CmsUser user = cms.readUser(creatorId); 519 creatorName = user.getName(); 520 } catch (CmsException e) { 521 // ignore, use id as name 522 } 523 try { 524 CmsUser user = cms.readUser(modifierId); 525 modifierName = user.getName(); 526 } catch (CmsException e) { 527 // ignore, use id as name 528 } 529 530 addPropertyString(tm, result, typeId, filter, PropertyIds.CREATED_BY, creatorName); 531 addPropertyString(tm, result, typeId, filter, PropertyIds.LAST_MODIFIED_BY, modifierName); 532 objectInfo.setCreatedBy(creatorName); 533 534 addPropertyId(tm, result, typeId, filter, PropertyIds.SOURCE_ID, relation.getSourceId().toString()); 535 addPropertyId(tm, result, typeId, filter, PropertyIds.TARGET_ID, relation.getTargetId().toString()); 536 537 // creation and modification date 538 GregorianCalendar lastModified = millisToCalendar(resource.getDateLastModified()); 539 GregorianCalendar created = millisToCalendar(resource.getDateCreated()); 540 541 addPropertyDateTime(tm, result, typeId, filter, PropertyIds.CREATION_DATE, created); 542 addPropertyDateTime(tm, result, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified); 543 objectInfo.setCreationDate(created); 544 objectInfo.setLastModificationDate(lastModified); 545 546 // change token - always null 547 addPropertyString(tm, result, typeId, filter, PropertyIds.CHANGE_TOKEN, null); 548 549 // base type and type name 550 addPropertyId(tm, result, typeId, filter, PropertyIds.BASE_TYPE_ID, BaseTypeId.CMIS_RELATIONSHIP.value()); 551 addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_TYPE_ID, typeId); 552 objectInfo.setHasParent(false); 553 return result; 554 } catch (Exception e) { 555 if (e instanceof CmisBaseException) { 556 throw (CmisBaseException)e; 557 } 558 throw new CmisRuntimeException(e.getMessage(), e); 559 } 560 } 561 562 /** 563 * Creates a user-readable name from the given relation object.<p> 564 * 565 * @param relation the relation object 566 * 567 * @return the readable name 568 */ 569 protected String createReadableName(CmsRelation relation) { 570 571 return relation.getType().getName() 572 + "[ " 573 + relation.getSourcePath() 574 + " -> " 575 + relation.getTargetPath() 576 + " ]"; 577 } 578 579 /** 580 * Extracts the source/target ids and the type from a relation id.<p> 581 * 582 * @param id the relation id 583 * 584 * @return the relation key object 585 */ 586 protected RelationKey parseRelationKey(String id) { 587 588 Matcher matcher = RELATION_PATTERN.matcher(id); 589 matcher.find(); 590 CmsUUID src = new CmsUUID(matcher.group(1)); 591 CmsUUID tgt = new CmsUUID(matcher.group(2)); 592 String tp = matcher.group(3); 593 return new RelationKey(src, tgt, tp); 594 } 595 596 /** 597 * Creates a relation id from the given OpenCms relation object.<p> 598 * 599 * @param relation the OpenCms relation object 600 * 601 * @return the relation id 602 */ 603 String createKey(CmsRelation relation) { 604 605 return createKey(relation.getSourceId(), relation.getTargetId(), relation.getType().getName()); 606 } 607 608}