001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.ade.publish; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsResource; 032import org.opencms.file.CmsResourceFilter; 033import org.opencms.file.types.CmsResourceTypeBinary; 034import org.opencms.file.types.CmsResourceTypeImage; 035import org.opencms.file.types.CmsResourceTypePlain; 036import org.opencms.file.types.CmsResourceTypePointer; 037import org.opencms.file.types.CmsResourceTypeXmlContent; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsLog; 040import org.opencms.main.OpenCms; 041import org.opencms.relations.CmsRelation; 042import org.opencms.relations.CmsRelationFilter; 043import org.opencms.relations.CmsRelationType; 044import org.opencms.util.CmsUUID; 045 046import java.util.Collection; 047import java.util.Collections; 048import java.util.HashMap; 049import java.util.List; 050import java.util.Map; 051import java.util.Set; 052 053import org.apache.commons.logging.Log; 054 055import com.google.common.base.Predicate; 056import com.google.common.collect.Lists; 057import com.google.common.collect.Maps; 058import com.google.common.collect.Sets; 059 060/** 061 * Helper class for finding all related resources for a set of resources to be published, for use with the new ADE publish dialog.<p> 062 */ 063public class CmsPublishRelationFinder { 064 065 /** 066 * A map from resources to sets of resources, which automtically instantiates an empty set when accessing a key that 067 * doesn't exist via get().<p> 068 */ 069 public static class ResourceMap extends HashMap<CmsResource, Set<CmsResource>> { 070 071 /** Serial version id. */ 072 private static final long serialVersionUID = 1L; 073 074 /** 075 * Constructor.<p> 076 */ 077 public ResourceMap() { 078 079 super(); 080 } 081 082 /** 083 * Creates a new resource map based on this instance while filtering some elements out.<p> 084 * 085 * The given predicate is used to check whether any single resource should be kept. If it returns 086 * false for a top-level resource (map key), the parent will be removed and all its children added as 087 * keys. If it returns false for a map value, the value will be removed for its key. 088 * 089 * @param pred predicate to check whether resources should be kept 090 * @return the new filtered resource map 091 */ 092 public ResourceMap filter(Predicate<CmsResource> pred) { 093 094 ResourceMap result = new ResourceMap(); 095 for (CmsResource key : keySet()) { 096 if (pred.apply(key)) { 097 result.get(key); 098 for (CmsResource value : get(key)) { 099 if (pred.apply(value)) { 100 result.get(key).add(value); 101 } 102 } 103 } else { 104 for (CmsResource value : get(key)) { 105 if (pred.apply(value)) { 106 result.get(value); 107 } 108 } 109 110 } 111 } 112 return result; 113 } 114 115 /** 116 * @see java.util.HashMap#get(java.lang.Object) 117 */ 118 @Override 119 public Set<CmsResource> get(Object res) { 120 121 Set<CmsResource> result = super.get(res); 122 if (result == null) { 123 result = Sets.newHashSet(); 124 put((CmsResource)res, result); 125 } 126 return result; 127 } 128 129 /** 130 * Returns the sum of all sizes of set values.<p> 131 * 132 * @return the total size 133 */ 134 public int totalSize() { 135 136 int result = 0; 137 for (Map.Entry<CmsResource, Set<CmsResource>> entry : entrySet()) { 138 result += entry.getValue().size(); 139 } 140 return result; 141 } 142 143 } 144 145 /** The log instance for this class. */ 146 private static final Log LOG = CmsLog.getLog(CmsPublishRelationFinder.class); 147 148 /** The resource types whose resources will be also added as related resources, even if the relation pointing to them is a weak relation.<p> */ 149 private static final String[] VALID_WEAK_RELATION_TARGET_TYPES = { 150 CmsResourceTypePlain.getStaticTypeName(), 151 CmsResourceTypeImage.getStaticTypeName(), 152 CmsResourceTypePointer.getStaticTypeName(), 153 CmsResourceTypeBinary.getStaticTypeName()}; 154 155 /** The CMS context used by this object. */ 156 private CmsObject m_cms; 157 158 /** Flag which controls whether unchanged resources in the original resource list should be kept or removed. */ 159 private boolean m_keepOriginalUnchangedResources; 160 161 /** The original set of resources passed in the constructor. */ 162 private Set<CmsResource> m_originalResources; 163 164 /** The provider for additional related resources. */ 165 private I_CmsPublishRelatedResourceProvider m_relatedResourceProvider; 166 167 /** Cache for resources. */ 168 private Map<CmsUUID, CmsResource> m_resources = Maps.newHashMap(); 169 170 /** 171 * Creates a new instance.<p> 172 * 173 * @param cms the CMS context to use 174 * @param resources the resources for which the related resources should be found 175 * @param keepOriginalUnchangedResources true if unchanged resources from the original resource list should be kept 176 * @param relProvider provider for additional related resources 177 */ 178 public CmsPublishRelationFinder( 179 CmsObject cms, 180 Collection<CmsResource> resources, 181 boolean keepOriginalUnchangedResources, 182 I_CmsPublishRelatedResourceProvider relProvider) { 183 184 m_cms = cms; 185 // put resources in a map with the structure id as a key 186 m_originalResources = Sets.newHashSet(resources); 187 for (CmsResource res : resources) { 188 m_resources.put(res.getStructureId(), res); 189 } 190 m_keepOriginalUnchangedResources = keepOriginalUnchangedResources; 191 m_relatedResourceProvider = relProvider; 192 } 193 194 /** 195 * Gets the related resources in the form of a ResourceMap.<p> 196 * 197 * @return a ResourceMap which has resources from the original set of resources as keys, and sets of related resources as values 198 * 199 */ 200 public ResourceMap getPublishRelatedResources() { 201 202 ResourceMap related = computeRelatedResources(); 203 ResourceMap reachable = computeReachability(related); 204 ResourceMap publishRelatedResources = getChangedResourcesReachableFromOriginalResources(reachable); 205 removeNestedItemsFromTopLevel(publishRelatedResources); 206 //addParentFolders(publishRelatedResources); 207 removeUnchangedTopLevelResources(publishRelatedResources, reachable); 208 return publishRelatedResources; 209 } 210 211 /** 212 * Removes unchanged resources from the top level, and if they have children which do not occur anywhere else, 213 * moves these children to the top level.<p> 214 * 215 * @param publishRelatedResources the resource map to modify 216 * @param reachability the reachability map 217 */ 218 public void removeUnchangedTopLevelResources(ResourceMap publishRelatedResources, ResourceMap reachability) { 219 220 Set<CmsResource> unchangedParents = Sets.newHashSet(); 221 Set<CmsResource> childrenOfUnchangedParents = Sets.newHashSet(); 222 Set<CmsResource> other = Sets.newHashSet(); 223 for (CmsResource parent : publishRelatedResources.keySet()) { 224 if (isUnchangedAndShouldBeRemoved(parent)) { 225 unchangedParents.add(parent); 226 childrenOfUnchangedParents.addAll(publishRelatedResources.get(parent)); 227 } else { 228 other.add(parent); 229 other.addAll(publishRelatedResources.get(parent)); 230 } 231 } 232 233 // we want the resources which *only* occur as children of unchanged parents 234 childrenOfUnchangedParents.removeAll(other); 235 236 for (CmsResource parent : unchangedParents) { 237 publishRelatedResources.remove(parent); 238 } 239 240 // Try to find hierarchical relationships in childrenOfUnchangedParents 241 while (findAndMoveParentWithChildren(childrenOfUnchangedParents, reachability, publishRelatedResources)) { 242 // do nothing 243 } 244 // only the resources with no 'children' are left, transfer them to the target map 245 for (CmsResource remainingResource : childrenOfUnchangedParents) { 246 publishRelatedResources.get(remainingResource); 247 } 248 } 249 250 /** 251 * Computes the "reachability map", given the map of direct relations between resources.<p> 252 * 253 * @param relatedResources a map containing the direct relations between resources 254 * @return a map from resources to the sets of resources which are reachable via relations 255 */ 256 private ResourceMap computeReachability(ResourceMap relatedResources) { 257 258 ResourceMap result = new ResourceMap(); 259 for (CmsResource resource : relatedResources.keySet()) { 260 result.get(resource).add(resource); 261 result.get(resource).addAll(relatedResources.get(resource)); 262 } 263 int oldSize, newSize; 264 do { 265 ResourceMap newReachableResources = new ResourceMap(); 266 oldSize = result.totalSize(); 267 for (CmsResource source : result.keySet()) { 268 for (CmsResource target : result.get(source)) { 269 // need to check if the key is present, otherwise we may get a ConcurrentModificationException 270 if (result.containsKey(target)) { 271 newReachableResources.get(source).addAll(result.get(target)); 272 } 273 } 274 } 275 newSize = newReachableResources.totalSize(); 276 result = newReachableResources; 277 } while (oldSize < newSize); 278 return result; 279 } 280 281 /** 282 * Gets a ResourceMap which contains, for each resource reachable from the original set of resources, the directly related resources.<p> 283 * 284 * @return a map from resources to their directly related resources 285 */ 286 private ResourceMap computeRelatedResources() { 287 288 ResourceMap relatedResources = new ResourceMap(); 289 Set<CmsResource> resourcesToProcess = Sets.newHashSet(m_originalResources); 290 Set<CmsResource> processedResources = Sets.newHashSet(); 291 while (!resourcesToProcess.isEmpty()) { 292 CmsResource currentResource = resourcesToProcess.iterator().next(); 293 resourcesToProcess.remove(currentResource); 294 processedResources.add(currentResource); 295 if (!currentResource.getState().isDeleted()) { 296 Set<CmsResource> directlyRelatedResources = getDirectlyRelatedResources(currentResource); 297 for (CmsResource target : directlyRelatedResources) { 298 if (m_cms.existsResource(target.getStructureId(), CmsResourceFilter.ALL.addRequireVisible())) { 299 if (!processedResources.contains(target)) { 300 resourcesToProcess.add(target); 301 } 302 relatedResources.get(currentResource).add(target); 303 } 304 } 305 } else { 306 if (CmsResourceTypeXmlContent.isXmlContent(currentResource) 307 && CmsResourceTypeXmlContent.isPossiblyDetailContent(currentResource)) { 308 // special case for detail-only containers: If the corresponding detail content has been deleted and is going to be published, 309 // we will usually want to publish the detail-only containers as well. 310 List<CmsRelation> relations = getRelationsFromResource(currentResource); 311 for (CmsRelation relation : relations) { 312 if (relation.getType() == CmsRelationType.DETAIL_ONLY) { 313 CmsResource target = getResource(relation.getTargetId()); 314 if (target != null) { 315 // only add them to the related resources, no further recursive processing 316 relatedResources.get(currentResource).add(target); 317 } 318 } 319 } 320 } 321 } 322 } 323 return relatedResources; 324 } 325 326 /** 327 * Tries to find a parent with related children in a set, and moves them to a result ResourceMap.<p> 328 * 329 * @param originalSet the original set 330 * @param reachability the reachability ResourceMap 331 * @param result the target ResourceMap to move the parent/children to 332 * 333 * @return true if a parent with children could be found (and moved) 334 */ 335 private boolean findAndMoveParentWithChildren( 336 Set<CmsResource> originalSet, 337 ResourceMap reachability, 338 ResourceMap result) { 339 340 for (CmsResource parent : originalSet) { 341 Set<CmsResource> reachableResources = reachability.get(parent); 342 Set<CmsResource> children = Sets.newHashSet(); 343 if (reachableResources.size() > 1) { 344 for (CmsResource potentialChild : reachableResources) { 345 if ((potentialChild != parent) && originalSet.contains(potentialChild)) { 346 children.add(potentialChild); 347 } 348 } 349 if (children.size() > 0) { 350 result.get(parent).addAll(children); 351 originalSet.removeAll(children); 352 originalSet.remove(parent); 353 return true; 354 } 355 } 356 } 357 return false; 358 359 } 360 361 /** 362 * Gets the resources which are reachable from the original set of resources and are not unchanged.<p> 363 * 364 * @param reachable the resource map of reachable resources 365 * @return the resources which are unchanged and reachable from the original set of resources 366 */ 367 private ResourceMap getChangedResourcesReachableFromOriginalResources(ResourceMap reachable) { 368 369 ResourceMap publishRelatedResources = new ResourceMap(); 370 for (CmsResource res : m_originalResources) { 371 Collection<CmsResource> reachableItems = reachable.get(res); 372 List<CmsResource> changedItems = Lists.newArrayList(); 373 for (CmsResource item : reachableItems) { 374 if (!isUnchangedAndShouldBeRemoved(item) && !item.getStructureId().equals(res.getStructureId())) { 375 changedItems.add(item); 376 } 377 } 378 publishRelatedResources.get(res).addAll(changedItems); 379 } 380 return publishRelatedResources; 381 } 382 383 /** 384 * Fetches the directly related resources for a given resource.<p> 385 * 386 * @param currentResource the resource for which to get the related resources 387 * @return the directly related resources 388 */ 389 private Set<CmsResource> getDirectlyRelatedResources(CmsResource currentResource) { 390 391 Set<CmsResource> directlyRelatedResources = Sets.newHashSet(); 392 List<CmsRelation> relations = getRelationsFromResource(currentResource); 393 for (CmsRelation relation : relations) { 394 LOG.info("Trying to read resource for relation " + relation.getTargetPath()); 395 CmsResource target = getResource(relation.getTargetId()); 396 if (target != null) { 397 if (relation.getType().isStrong() || shouldAddWeakRelationTarget(target)) { 398 directlyRelatedResources.add(target); 399 } 400 } 401 } 402 try { 403 CmsResource parentFolder = m_cms.readParentFolder(currentResource.getStructureId()); 404 if (parentFolder != null) { // parent folder of root folder is null 405 if (parentFolder.getState().isNew() || currentResource.isFile()) { 406 directlyRelatedResources.add(parentFolder); 407 } 408 } 409 } catch (CmsException e) { 410 LOG.error( 411 "Error processing parent folder for " + currentResource.getRootPath() + ": " + e.getLocalizedMessage(), 412 e); 413 } 414 415 try { 416 directlyRelatedResources.addAll( 417 m_relatedResourceProvider.getAdditionalRelatedResources(m_cms, currentResource)); 418 } catch (Exception e) { 419 LOG.error( 420 "Error processing additional related resource for " 421 + currentResource.getRootPath() 422 + ": " 423 + e.getLocalizedMessage(), 424 e); 425 } 426 return directlyRelatedResources; 427 } 428 429 /** 430 * Reads the relations from a given resource, and returns an empty list if an error occurs while reading them.<p> 431 * 432 * @param currentResource the resource for which to get the relation 433 * @return the outgoing relations 434 */ 435 private List<CmsRelation> getRelationsFromResource(CmsResource currentResource) { 436 437 try { 438 return m_cms.readRelations(CmsRelationFilter.relationsFromStructureId(currentResource.getStructureId())); 439 } catch (CmsException e) { 440 return Collections.emptyList(); 441 } 442 } 443 444 /** 445 * Reads a resource with a given id, but will get a resource from a cache if it has already been read before.<p> 446 * If an error occurs, null will be returned. 447 * 448 * @param structureId the structure id 449 * @return the resource with the given structure id 450 */ 451 private CmsResource getResource(CmsUUID structureId) { 452 453 CmsResource resource = m_resources.get(structureId); 454 if (resource == null) { 455 try { 456 resource = m_cms.readResource(structureId, CmsResourceFilter.ALL); 457 m_resources.put(structureId, resource); 458 } catch (CmsException e) { 459 LOG.info(e.getLocalizedMessage(), e); 460 } 461 } 462 return resource; 463 } 464 465 /** 466 * Checks if the resource is unchanged *and* should be removed.<p> 467 * 468 * @param item the resource to check 469 * @return true if the resource is unchanged and should be removed 470 */ 471 private boolean isUnchangedAndShouldBeRemoved(CmsResource item) { 472 473 if (item.getState().isUnchanged()) { 474 return !m_keepOriginalUnchangedResources || !m_originalResources.contains(item); 475 } 476 return false; 477 } 478 479 /** 480 * Removes those resources as keys from the resource map which also occur as related resources under a different key.<p> 481 * 482 * @param publishRelatedResources the resource map from which to remove the duplicate items 483 */ 484 private void removeNestedItemsFromTopLevel(ResourceMap publishRelatedResources) { 485 486 Set<CmsResource> toDelete = Sets.newHashSet(); 487 for (CmsResource parent : publishRelatedResources.keySet()) { 488 if (toDelete.contains(parent)) { 489 continue; 490 } 491 for (CmsResource child : publishRelatedResources.get(parent)) { 492 if (publishRelatedResources.containsKey(child)) { 493 toDelete.add(child); 494 } 495 } 496 } 497 for (CmsResource delResource : toDelete) { 498 publishRelatedResources.remove(delResource); 499 } 500 } 501 502 /** 503 * Checks if the resource should be added to the related resources even if the relation pointing to it is a weak relation.<p> 504 * 505 * @param weakRelationTarget the relation target resource 506 * @return true if the resource should be added as a related resource 507 */ 508 private boolean shouldAddWeakRelationTarget(CmsResource weakRelationTarget) { 509 510 for (String typeName : VALID_WEAK_RELATION_TARGET_TYPES) { 511 if (OpenCms.getResourceManager().matchResourceType(typeName, weakRelationTarget.getTypeId())) { 512 return true; 513 } 514 } 515 return false; 516 } 517}