001/* 002 * Copyright 2019 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.contentstree; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.contenttype.ContentType; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.data.ContentValue; 039import org.ametys.cms.data.type.ModelItemTypeConstants; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ModifiableContent; 042import org.ametys.core.ui.Callable; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 045import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 046import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 047import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 048import org.ametys.plugins.repository.model.CompositeDefinition; 049import org.ametys.plugins.repository.model.RepeaterDefinition; 050import org.ametys.runtime.model.ModelItem; 051import org.ametys.runtime.model.ModelItemContainer; 052import org.ametys.runtime.plugin.component.AbstractLogEnabled; 053 054/** 055 * Helper for contents tree 056 * 057 */ 058public class ContentsTreeHelper extends AbstractLogEnabled implements Component, Serviceable 059{ 060 /** The Avalon role */ 061 public static final String ROLE = ContentsTreeHelper.class.getName(); 062 063 /** The ametys object resolver instance */ 064 protected AmetysObjectResolver _ametysResolver; 065 /** The tree configuration EP instance */ 066 protected TreeExtensionPoint _treeExtensionPoint; 067 /** The content type EP instance */ 068 protected ContentTypeExtensionPoint _contentTypesEP; 069 /** The content types helper instance */ 070 protected ContentTypesHelper _contentTypesHelper; 071 072 @Override 073 public void service(ServiceManager smanager) throws ServiceException 074 { 075 _ametysResolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 076 _treeExtensionPoint = (TreeExtensionPoint) smanager.lookup(TreeExtensionPoint.ROLE); 077 _contentTypesEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 078 _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 079 } 080 081 /** 082 * Determines if the content has children contents according the tree configuration 083 * @param content the root content 084 * @param treeConfiguration the tree configuration 085 * @return true if the content has children contents 086 */ 087 public boolean hasChildrenContent(Content content, TreeConfiguration treeConfiguration) 088 { 089 return !getChildrenContent(content, treeConfiguration).isEmpty(); 090 } 091 092 /** 093 * Get the children contents according the tree configuration 094 * @param parentContent the root content 095 * @param treeConfiguration the tree configuration 096 * @return the children content for each child attributes 097 */ 098 public Map<String, List<Content>> getChildrenContent(Content parentContent, TreeConfiguration treeConfiguration) 099 { 100 Map<String, List<Content>> childrenContent = new HashMap<>(); 101 102 // For each content type of the content 103 for (String contentTypeId : parentContent.getTypes()) 104 { 105 // Loop over all possible elements for the tree 106 for (TreeConfigurationElements treeConfigurationElements : treeConfiguration.getElements()) 107 { 108 // Check for a match between the element and the content type of the content 109 for (TreeConfigurationContentType treeConfigurationContentType : treeConfigurationElements.getContentTypesConfiguration()) 110 { 111 if (treeConfigurationContentType.getContentTypesIds().contains(contentTypeId)) 112 { 113 ContentType contentType = _contentTypesEP.getExtension(contentTypeId); 114 115 // Add all required children for this element 116 for (TreeConfigurationElementsChild treeConfigurationElementsChild : treeConfigurationElements.getChildren()) 117 { 118 if (treeConfigurationElementsChild instanceof AttributeTreeConfigurationElementsChild) 119 { 120 // Get the attribute 121 Map<String, List<Content>> contents = _handleAttributeTreeConfigurationElementsChild(contentType, parentContent, (AttributeTreeConfigurationElementsChild) treeConfigurationElementsChild, treeConfiguration); 122 _merge(childrenContent, contents); 123 } 124 else 125 { 126 throw new IllegalArgumentException("The child configuration element class <" + treeConfigurationElementsChild + "> is not supported in tree '" + treeConfiguration.getId() + "'"); 127 } 128 } 129 } 130 } 131 132 } 133 } 134 135 return childrenContent; 136 } 137 138 /** 139 * Get the children contents according the tree configuration 140 * @param contentId the parent content 141 * @param path the path of the content in the current tree 142 * @param treeId the tree configuration 143 * @return the children content 144 */ 145 @Callable 146 public Map<String, Object> getChildrenContent(String contentId, List<String> path, String treeId) 147 { 148 TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId); 149 Content parentContent = _getParentContent(contentId); 150 151 Map<String, Object> infos = new HashMap<>(); 152 153 _addChildren(parentContent, path, treeConfiguration, infos); 154 155 infos.putAll(getNodeInformations(contentId, path)); 156 157 return infos; 158 } 159 160 /** 161 * Add the json info to list the children of a content 162 * @param content The content 163 * @param path the content path in the current tree 164 * @param treeConfiguration The current tree configuration 165 * @param infos The infos where to add the children key 166 */ 167 protected void _addChildren(Content content, List<String> path, TreeConfiguration treeConfiguration, Map<String, Object> infos) 168 { 169 Map<String, List<Content>> children = getChildrenContent(content, treeConfiguration); 170 171 boolean hasAtLeastOneAutoExpand = hasAutoExpandTargets(treeConfiguration); 172 173 List<Map<String, Object>> childrenInfos = new ArrayList<>(); 174 infos.put("children", childrenInfos); 175 176 for (String attributePath : children.keySet()) 177 { 178 for (Content childContent : children.get(attributePath)) 179 { 180 boolean expand = hasAtLeastOneAutoExpand && !isAnAutoExpandTarget(treeConfiguration, childContent); 181 182 List<String> contentPath = new ArrayList<>(path); 183 contentPath.add(childContent.getId()); 184 185 Map<String, Object> childInfo = content2Json(childContent, contentPath); 186 childInfo.put("metadataPath", attributePath); 187 childInfo.put("expanded", expand); 188 189 if (expand) 190 { 191 _addChildren(childContent, contentPath, treeConfiguration, childInfo); 192 } 193 194 if (!hasChildrenContent(childContent, treeConfiguration)) 195 { 196 // childInfo.put("leaf", true); 197 childInfo.put("children", Collections.EMPTY_LIST); 198 } 199 else 200 { 201 childInfo.put("leaf", false); 202 childInfo.put("isExpanded", false); 203 } 204 205 childrenInfos.add(childInfo); 206 } 207 } 208 209 } 210 211 /** 212 * Get the path of children content which match filter regexp 213 * @param parentContentId The id of content to start search 214 * @param treeId The id of tree configuration 215 * @param value the value to match 216 * @return the matching paths composed by contents id separated by ';' 217 */ 218 @Callable 219 public List<String> filterChildrenContentByRegExp(String parentContentId, String treeId, String value) 220 { 221 List<String> matchingPaths = new ArrayList<>(); 222 223 Content parentContent = _ametysResolver.resolveById(parentContentId); 224 TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId); 225 226 String toMatch = StringUtils.stripAccents(value.toLowerCase()).trim(); 227 228 Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(parentContent, treeConfiguration); 229 for (List<Content> childrenContent : childrenContentByAttributes.values()) 230 { 231 for (Content childContent : childrenContent) 232 { 233 _getMatchingContents(childContent, toMatch, treeConfiguration, matchingPaths, parentContentId); 234 } 235 } 236 237 return matchingPaths; 238 } 239 240 private void _getMatchingContents(Content content, String value, TreeConfiguration treeConfiguration, List<String> matchingPaths, String parentPath) 241 { 242 if (isContentMatching(content, value)) 243 { 244 matchingPaths.add(parentPath + ";" + content.getId()); 245 } 246 247 Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(content, treeConfiguration); 248 for (List<Content> childrenContent : childrenContentByAttributes.values()) 249 { 250 for (Content childContent : childrenContent) 251 { 252 _getMatchingContents(childContent, value, treeConfiguration, matchingPaths, parentPath + ";" + content.getId()); 253 } 254 } 255 } 256 257 /** 258 * Determines if content matches the filter regexp 259 * @param content the content 260 * @param value the value to match 261 * @return true if the content match 262 */ 263 protected boolean isContentMatching(Content content, String value) 264 { 265 String title = StringUtils.stripAccents(content.getTitle().toLowerCase()); 266 return title.contains(value); 267 } 268 269 /** 270 * Get the root node informations 271 * @param contentId The content 272 * @param treeId The contents tree id 273 * @return The informations 274 */ 275 @Callable 276 public Map<String, Object> getRootNodeInformations(String contentId, String treeId) 277 { 278 TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId); 279 Content content = _ametysResolver.resolveById(contentId); 280 281 Map<String, Object> nodeInformations = content2Json(content, List.of(contentId)); 282 nodeInformations.put("id", "root"); // Root id should not be random, for delete op 283 _addChildren(content, List.of(contentId), treeConfiguration, nodeInformations); // auto expand first level + recursively if necessary 284 285 return nodeInformations; 286 } 287 288 /** 289 * Get the node informations 290 * @param contentId The content 291 * @param path The path of the content in the tree 292 * @return The informations 293 */ 294 @Callable 295 public Map<String, Object> getNodeInformations(String contentId, List<String> path) 296 { 297 Content content = _ametysResolver.resolveById(contentId); 298 return content2Json(content, path); 299 } 300 301 /** 302 * Get the default JSON representation of a content of the tree 303 * @param content the content 304 * @param path The path of the content in the tree 305 * @return the content as JSON 306 */ 307 protected Map<String, Object> content2Json(Content content, List<String> path) 308 { 309 Map<String, Object> infos = new HashMap<>(); 310 311 infos.put("id", "random-id-" + org.ametys.core.util.StringUtils.generateKey() + "-" + Math.round(Math.random() * 10_000)); 312 313 infos.put("contentId", content.getId()); 314 infos.put("contenttypesIds", content.getTypes()); 315 infos.put("name", content.getName()); 316 infos.put("title", content.getTitle()); 317 318 infos.put("iconGlyph", _contentTypesHelper.getIconGlyph(content)); 319 infos.put("iconDecorator", _contentTypesHelper.getIconDecorator(content)); 320 infos.put("iconSmall", _contentTypesHelper.getSmallIcon(content)); 321 infos.put("iconMedium", _contentTypesHelper.getMediumIcon(content)); 322 infos.put("iconLarge", _contentTypesHelper.getLargeIcon(content)); 323 324 return infos; 325 } 326 327 /** 328 * Get the default JSON representation of a child content 329 * @param content the content 330 * @param path The path of the content in the tree 331 * @param attributePath the path of attribute holding this content 332 * @return the content as JSON 333 */ 334 public Map<String, Object> childContent2Json(Content content, List<String> path, String attributePath) 335 { 336 Map<String, Object> childInfo = content2Json(content, path); 337 childInfo.put("metadataPath", attributePath); 338 return childInfo; 339 } 340 341 private void _merge(Map<String, List<Content>> childrenContent, Map<String, List<Content>> contents) 342 { 343 for (String key : contents.keySet()) 344 { 345 if (!childrenContent.containsKey(key)) 346 { 347 childrenContent.put(key, new ArrayList<>()); 348 } 349 350 List<Content> contentsList = childrenContent.get(key); 351 contentsList.addAll(contents.get(key)); 352 } 353 } 354 355 private Map<String, List<Content>> _handleAttributeTreeConfigurationElementsChild(ContentType contentType, ModelAwareDataHolder dataHolder, AttributeTreeConfigurationElementsChild attributeTreeConfigurationElementsChild, TreeConfiguration treeConfiguration) 356 { 357 Map<String, List<Content>> childrenContent = new HashMap<>(); 358 359 String attributePath = attributeTreeConfigurationElementsChild.getPath(); 360 361 try 362 { 363 Map<String, List<Content>> contents = _handleAttribute(contentType, dataHolder, attributePath); 364 _merge(childrenContent, contents); 365 } 366 catch (Exception e) 367 { 368 throw new IllegalArgumentException("An error occured on the tree configuration '" + treeConfiguration.getId() + "' getting for metadata '" + attributePath + "' on content type '" + contentType.getId() + "'", e); 369 } 370 371 return childrenContent; 372 } 373 374 private Map<String, List<Content>> _handleAttribute(ModelItemContainer modelItemContainer, ModelAwareDataHolder dataHolder, String attributePath) 375 { 376 Map<String, List<Content>> childrenContent = new HashMap<>(); 377 378 String currentModelItemName = StringUtils.substringBefore(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 379 380 ModelItem currentModelItem = modelItemContainer.getChild(currentModelItemName); 381 if (currentModelItem == null) 382 { 383 throw new IllegalArgumentException("No attribute definition for " + currentModelItemName); 384 } 385 386 387 if (dataHolder.hasValue(currentModelItemName)) 388 { 389 if (currentModelItem instanceof RepeaterDefinition) 390 { 391 ModelAwareRepeater repeater = dataHolder.getRepeater(currentModelItemName); 392 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 393 { 394 String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 395 Map<String, List<Content>> contents = _handleAttribute((RepeaterDefinition) currentModelItem, entry, subMetadataId); 396 397 _merge(childrenContent, contents); 398 } 399 } 400 else if (currentModelItem instanceof CompositeDefinition) 401 { 402 ModelAwareComposite metadata = dataHolder.getComposite(currentModelItemName); 403 404 String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 405 Map<String, List<Content>> contents = _handleAttribute((CompositeDefinition) currentModelItem, metadata, subMetadataId); 406 407 _merge(childrenContent, contents); 408 } 409 else if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(currentModelItem.getType().getId())) 410 { 411 ContentValue[] contentValues = dataHolder.getValue(currentModelItemName); 412 for (ContentValue contentValue : contentValues) 413 { 414 String key = currentModelItem.getPath(); 415 Optional<ModifiableContent> optContent = contentValue.getContentIfExists(); 416 if (optContent.isPresent()) 417 { 418 childrenContent.computeIfAbsent(key, k -> new ArrayList<>()).add(optContent.get()); 419 } 420 else 421 { 422 getLogger().warn("On a data holder, the attribute '{}' is referencing a unexisting content: '{}'", key, contentValue.getContentId()); 423 } 424 } 425 } 426 else 427 { 428 throw new IllegalArgumentException("The metadata definition for " + currentModelItem.getPath() + " is not a content"); 429 } 430 } 431 432 return childrenContent; 433 } 434 435 /** 436 * Get the tree configuration 437 * @param treeId the tree id 438 * @return the tree configuration 439 */ 440 protected TreeConfiguration _getTreeConfiguration(String treeId) 441 { 442 if (StringUtils.isBlank(treeId)) 443 { 444 throw new IllegalArgumentException("The tree information cannot be obtain, because 'tree' is null"); 445 } 446 447 TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId); 448 if (treeConfiguration == null) 449 { 450 throw new IllegalArgumentException("There is no tree configuration for '" + treeId + "'"); 451 } 452 return treeConfiguration; 453 } 454 455 /** 456 * Get the parent content of a tree 457 * @param parentId the parent id 458 * @return the parent content of a tree 459 * @throws IllegalArgumentException if an exception occurred 460 */ 461 protected Content _getParentContent(String parentId) throws IllegalArgumentException 462 { 463 if (StringUtils.isBlank(parentId)) 464 { 465 throw new IllegalArgumentException("The tree information cannot be obtain, because 'node' is null"); 466 } 467 468 try 469 { 470 return _ametysResolver.resolveById(parentId); 471 } 472 catch (Exception e) 473 { 474 throw new IllegalArgumentException("The tree configuration cannot be used on an object that is not a content: " + parentId, e); 475 } 476 477 } 478 479 /** 480 * Should auto expand until some kind of node? 481 * @param treeConfiguration The tree configuration 482 * @return true if should auto expand 483 */ 484 protected boolean hasAutoExpandTargets(TreeConfiguration treeConfiguration) 485 { 486 return treeConfiguration.getElements() 487 .stream() 488 .map(TreeConfigurationElements::getContentTypesConfiguration) 489 .flatMap(Collection::stream) 490 .anyMatch(TreeConfigurationContentType::autoExpandTarget); 491 } 492 493 /** 494 * Should auto expand until some kind of node? 495 * @param treeConfiguration The tree configuration 496 * @param content The content involved 497 * @return true if should auto expand to it 498 */ 499 protected boolean isAnAutoExpandTarget(TreeConfiguration treeConfiguration, Content content) 500 { 501 List<String> contentTypes = Arrays.asList(content.getTypes()); 502 503 return treeConfiguration.getElements() 504 .stream() 505 .map(TreeConfigurationElements::getContentTypesConfiguration) 506 .flatMap(Collection::stream) 507 .filter(TreeConfigurationContentType::autoExpandTarget) 508 .map(TreeConfigurationContentType::getContentTypesIds) 509 .anyMatch(ct -> { Set<String> hs = new HashSet<>(ct); hs.retainAll(contentTypes); return hs.size() > 0; }); 510 } 511}