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