001/* 002 * Copyright 2017 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.cms.content.referencetable; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashSet; 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Optional; 024import java.util.Set; 025 026import org.apache.avalon.framework.activity.Disposable; 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031 032import org.ametys.cms.contenttype.ContentType; 033import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 034import org.ametys.cms.contenttype.MetadataDefinition; 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.repository.ModifiableContent; 037import org.ametys.core.ui.Callable; 038import org.ametys.plugins.repository.AmetysObjectResolver; 039import org.ametys.plugins.repository.metadata.CompositeMetadata; 040import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 041import org.ametys.runtime.plugin.component.AbstractLogEnabled; 042 043import com.google.common.collect.BiMap; 044import com.google.common.collect.HashBiMap; 045 046/** 047 * Helper component for computing information about hierarchy of reference table Contents. 048 * <br><br> 049 * 050 * At the startup of the Ametys application, you must call {@link #registerRelation(ContentType, ContentType)} in order to register a <b>relation</b> between a <b>parent</b> content type and its <b>child</b> content type.<br> 051 * When all relations are registered, one or several <b>hierarchy(ies)</b> can be inferred, following some basic rules:<br> 052 * <ul> 053 * <li>A hierarchy of two or more content types cannot be cyclic</li> 054 * <li>A content type can have itself as its parent content type</li> 055 * <li>A content type cannot have two different parent content types</li> 056 * <li>A content type can have only one content type as children, plus possibly itself</li> 057 * </ul> 058 * From each hierarchy of content types, a <b>tree</b> of contents can be inferred.<br> 059 * <br> 060 * For instance, the following examples of hierarchy are valid (where <b>X←Y</b> means 'X is the parent content type of Y'; <br>and <b>⤹Z</b> means 'Z is the parent content type of Z'): 061 * <ul> 062 * <li>B←A</li> 063 * <li>E←D←C←B←A</li> 064 * <li>⤹B←A (content type B defines two different child content types, but one is itself, which is allowed)</li> 065 * <li>⤹E←D←C←B←A</li> 066 * <li>⤹A</li> 067 * </ul> 068 * ; and the following examples of hierarchy are invalid: 069 * <ul> 070 * <li>C←B and C←A (a content type cannot have multiple content types as children, which are not itself)</li> 071 * <li>C←A and B←A (a content type cannot have two different parent content types)</li> 072 * <li>⤹A and B←A (a content type cannot have two different parent content types, even if one is itself)</li> 073 * <li>A←B and B←A (cyclic hierarchy)</li> 074 * <li>A←C←B←A (cyclic hierarchy)</li> 075 * </ul> 076 */ 077public class HierarchicalReferenceTablesHelper extends AbstractLogEnabled implements Component, Serviceable, Disposable 078{ 079 /** The Avalon role */ 080 public static final String ROLE = HierarchicalReferenceTablesHelper.class.getName(); 081 082 /** The extension point for content types */ 083 protected ContentTypeExtensionPoint _contentTypeEP; 084 /** The Ametys objet resolver */ 085 protected AmetysObjectResolver _resolver; 086 087 /** The map parent -> child (excepted content types pointing on themselves) */ 088 private BiMap<ContentType, ContentType> _childByContentType = HashBiMap.create(); 089 /** The content types pointing at themselves */ 090 private Set<ContentType> _autoReferencingContentTypes = new HashSet<>(); 091 /** The map leafContentType -> topLevelContentType */ 092 private BiMap<ContentType, ContentType> _topLevelTypeByLeafType = HashBiMap.create(); 093 094 @Override 095 public void service(ServiceManager manager) throws ServiceException 096 { 097 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 098 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 099 } 100 101 @Override 102 public void dispose() 103 { 104 _childByContentType.clear(); 105 _autoReferencingContentTypes.clear(); 106 _topLevelTypeByLeafType.clear(); 107 } 108 109 /** 110 * Register a relation between a parent and its child, and update the internal model if it is a valid one. 111 * @param parent The parent content type 112 * @param child The child content type 113 * @return true if the relation is valid, i.e. in accordance with what was registered before 114 */ 115 public boolean registerRelation(ContentType parent, ContentType child) 116 { 117 if (parent.equals(child)) 118 { 119 _autoReferencingContentTypes.add(parent); 120 if (_childByContentType.containsKey(parent)) 121 { 122 // _topLevelTypeByLeafType does not need to be updated as another content type references it 123 } 124 else 125 { 126 // _topLevelTypeByLeafType needs to be updated as no other content type references it 127 _topLevelTypeByLeafType.put(parent, parent); 128 } 129 return true; 130 } 131 else if (_childByContentType.containsKey(parent)) 132 { 133 getLogger().error("Problem of definition of parent for content type '{}'. Its parent '{}' is already declared by '{}'. A content type cannot have multiple content types as children", child, parent, _childByContentType.get(parent)); 134 return false; 135 } 136 else if (_checkNoCycle(parent, child)) 137 { 138 // ok valid 139 // update _childByContentType 140 _childByContentType.put(parent, child); 141 142 // update _topLevelTypeByLeafType 143 boolean containsParentAsKey = _topLevelTypeByLeafType.containsKey(parent); 144 boolean containsChildAsValue = _topLevelTypeByLeafType.containsValue(child); 145 if (containsParentAsKey && containsChildAsValue) 146 { 147 // is currently something as {parent: other, other2: child}, which means the hierarchy is like: other -> parent -> child -> other2 148 // we now want {other2: other} 149 ContentType other = _topLevelTypeByLeafType.remove(parent); 150 ContentType other2 = _topLevelTypeByLeafType.inverse().get(child); 151 _topLevelTypeByLeafType.put(other2, other); 152 } 153 else if (containsParentAsKey) 154 { 155 // is currently something as {parent: other}, which means the hierarchy is like: other -> ... -> parent -> child 156 // we now want {child: other} 157 ContentType other = _topLevelTypeByLeafType.remove(parent); 158 _topLevelTypeByLeafType.put(child, other); 159 } 160 else if (containsChildAsValue) 161 { 162 // is currently something as {other: child}, which means the hierarchy is like: parent -> child -> ... -> other 163 // we now want {other: parent} 164 ContentType other = _topLevelTypeByLeafType.inverse().get(child); 165 _topLevelTypeByLeafType.put(other, parent); 166 } 167 else 168 { 169 _topLevelTypeByLeafType.put(child, parent); 170 } 171 return true; 172 } 173 else 174 { 175 // An error was logged in #_checkNoCycle method 176 return false; 177 } 178 } 179 180 /** 181 * Returns true if at least one hierarchy was registered (i.e. at least one content type defines a valid "parent" metadata) 182 * @return true if at least one hierarchy was registered 183 */ 184 public boolean hasAtLeastOneHierarchy() 185 { 186 return !_childByContentType.isEmpty() || !_autoReferencingContentTypes.isEmpty(); 187 } 188 189 /** 190 * Gets the top level content type for the given leaf content type (which defines the hierarchy) 191 * @param leafContentType the leaf cotnent type 192 * @return the top level content type for the given leaf content type 193 */ 194 public ContentType getTopLevelType(ContentType leafContentType) 195 { 196 return _topLevelTypeByLeafType.get(leafContentType); 197 } 198 199 private boolean _checkNoCycle(ContentType parent, ContentType child) 200 { 201 // at this stage, parent is not equal to child 202 203 Set<ContentType> contentTypesInHierarchy = new HashSet<>(); 204 contentTypesInHierarchy.add(child); 205 contentTypesInHierarchy.add(parent); 206 207 ContentType parentContentType = parent; 208 209 do 210 { 211 final ContentType currentContentType = parentContentType; 212 parentContentType = Optional.ofNullable(currentContentType) 213 .map(ContentType::getParentMetadata) 214 .map(MetadataDefinition::getContentType) 215 .map(_contentTypeEP::getExtension) 216 .filter(par -> !currentContentType.equals(par)) // if current equals to parent, set to null as there will be no cycle 217 .orElse(null); 218 if (contentTypesInHierarchy.contains(parentContentType)) 219 { 220 // there is a cycle, log an error and return false 221 getLogger().error("Problem of definition of parent for content type {}. It defines a cyclic hierarchy (The following content types are involved: {}).", child, contentTypesInHierarchy); 222 return false; 223 } 224 contentTypesInHierarchy.add(parentContentType); 225 } 226 while (parentContentType != null); 227 228 // no cycle, it is ok, return true 229 return true; 230 } 231 232 /** 233 * Returns true if the given content type has a child content type 234 * @param contentType The content type 235 * @return true if the given content type has a child content type 236 */ 237 public boolean hasChildContentType(ContentType contentType) 238 { 239 return _childByContentType.containsKey(contentType) || _autoReferencingContentTypes.contains(contentType); 240 } 241 242 /** 243 * Returs true if the given content type is hierarchical, i.e. is part of a hierarchy 244 * @param contentType The content type 245 * @return true if the given content type is hierarchical, i.e. is part of a hierarchy 246 */ 247 public boolean isHierarchical(ContentType contentType) 248 { 249 return _childByContentType.containsKey(contentType) || _childByContentType.containsValue(contentType) || _autoReferencingContentTypes.contains(contentType); 250 } 251 252 /** 253 * Returns true if the given content type is a leaf content type 254 * @param contentType The content type 255 * @return true if the given content type is a leaf content type 256 */ 257 public boolean isLeaf(ContentType contentType) 258 { 259 return _topLevelTypeByLeafType.containsKey(contentType); 260 } 261 262 /** 263 * Get the hierarchy of content types (distinct content types) 264 * @param leafContentTypeId The id of leaf content type 265 * @return The content types of hierarchy 266 */ 267 public Set<String> getHierarchicalContentTypes(String leafContentTypeId) 268 { 269 Set<String> hierarchicalTypes = new LinkedHashSet<>(); 270 hierarchicalTypes.add(leafContentTypeId); 271 BiMap<ContentType, ContentType> parentByContentType = _childByContentType.inverse(); 272 273 ContentType leafContentType = _contentTypeEP.getExtension(leafContentTypeId); 274 275 ContentType parentContentType = parentByContentType.get(leafContentType); 276 while (parentContentType != null) 277 { 278 hierarchicalTypes.add(parentContentType.getId()); 279 parentContentType = parentByContentType.get(parentContentType); 280 } 281 return hierarchicalTypes; 282 } 283 284 /** 285 * Get the path of reference table entry in its hierarchy 286 * @param refTableEntryId The id of entry 287 * @return The path from root parent 288 */ 289 @Callable 290 public String getPathInHierarchy(String refTableEntryId) 291 { 292 Content refTableEntry = _resolver.resolveById(refTableEntryId); 293 List<String> paths = new ArrayList<>(); 294 paths.add(refTableEntry.getName()); 295 296 String parentId = getParent(refTableEntry); 297 while (parentId != null) 298 { 299 Content parent = _resolver.resolveById(parentId); 300 paths.add(parent.getName()); 301 parentId = getParent(parent); 302 } 303 304 Collections.reverse(paths); 305 return org.apache.commons.lang3.StringUtils.join(paths, "/"); 306 } 307 308 /** 309 * Gets the content types the children of the given content can have. 310 * The result can contain 0, 1 or 2 content types 311 * @param refTableEntry The content 312 * @return the content types the children of the given content can have. 313 */ 314 public List<ContentType> getChildContentTypes(Content refTableEntry) 315 { 316 List<ContentType> result = new ArrayList<>(); 317 318 for (String cTypeId : refTableEntry.getTypes()) 319 { 320 ContentType cType = _contentTypeEP.getExtension(cTypeId); 321 if (_childByContentType.containsKey(cType)) 322 { 323 result.add(_childByContentType.get(cType)); 324 } 325 if (_autoReferencingContentTypes.contains(cType)) 326 { 327 result.add(cType); 328 } 329 330 if (!result.isEmpty()) 331 { 332 break; 333 } 334 } 335 336 return result; 337 } 338 339 /** 340 * Sets the "parent" metadata of the given content 341 * See also {@link ContentType#getParentMetadata()} 342 * @param content The content 343 * @param parent The value of the "parent" metadata 344 */ 345 public void setParentMetadata(ModifiableContent content, Content parent) 346 { 347 for (String cTypeId : content.getTypes()) 348 { 349 ContentType cType = _contentTypeEP.getExtension(cTypeId); 350 MetadataDefinition parentMetadata = cType.getParentMetadata(); 351 if (parentMetadata != null) 352 { 353 _setMetadata(parentMetadata, content, parent); 354 break; 355 } 356 } 357 } 358 359 private void _setMetadata(MetadataDefinition metadataDefinition, ModifiableContent content, Content value) 360 { 361 ModifiableCompositeMetadata metadataHolder = content.getMetadataHolder(); 362 String metadataName = metadataDefinition.getName(); 363 364 // Set metadata 365 metadataHolder.setMetadata(metadataName, value.getId()); 366 } 367 368 /** 369 * Returns the "parent" metadata value for the given content, or null if it is not defined for its content types 370 * See also {@link ContentType#getParentMetadata()} 371 * @param content The content 372 * @return the "parent" metadata value for the given content, or null 373 */ 374 public String getParent(Content content) 375 { 376 for (String cTypeId : content.getTypes()) 377 { 378 if (!_contentTypeEP.hasExtension(cTypeId)) 379 { 380 getLogger().warn("The content {} ({}) has unknown type '{}'", content.getId(), content.getTitle(), cTypeId); 381 continue; 382 } 383 ContentType cType = _contentTypeEP.getExtension(cTypeId); 384 MetadataDefinition parentMetadata = cType.getParentMetadata(); 385 if (parentMetadata != null) 386 { 387 CompositeMetadata metadataHolder = content.getMetadataHolder(); 388 String metadataName = parentMetadata.getName(); 389 if (metadataHolder.hasMetadata(metadataName)) 390 { 391 return content.getMetadataHolder().getString(metadataName); 392 } 393 } 394 } 395 return null; 396 } 397 398 /** 399 * Recursively returns the "parent" metadata value for the given content, i.e; will return the parent, the parent of the parent, etc. 400 * @param content The content 401 * @return all the parents of the given content 402 */ 403 public List<String> getAllParents(Content content) 404 { 405 List<String> parents = new ArrayList<>(); 406 407 Content currentContent = content; 408 String parentId = getParent(currentContent); 409 410 while (parentId != null) 411 { 412 if (_resolver.hasAmetysObjectForId(parentId)) 413 { 414 parents.add(parentId); 415 currentContent = _resolver.resolveById(parentId); 416 parentId = getParent(currentContent); 417 } 418 else 419 { 420 break; 421 } 422 } 423 424 return parents; 425 } 426}