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 treeId the tree configuration 142 * @return the children content 143 */ 144 @Callable 145 public Map<String, Object> getChildrenContent(String contentId, String treeId) 146 { 147 TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId); 148 Content parentContent = _getParentContent(contentId); 149 150 Map<String, Object> infos = new HashMap<>(); 151 152 _addChildren(parentContent, treeConfiguration, infos); 153 154 infos.putAll(getNodeInformations(contentId)); 155 156 return infos; 157 } 158 159 /** 160 * Add the json info to list the children of a content 161 * @param content The content 162 * @param treeConfiguration The current tree configuration 163 * @param infos The infos where to add the children key 164 */ 165 protected void _addChildren(Content content, TreeConfiguration treeConfiguration, Map<String, Object> infos) 166 { 167 Map<String, List<Content>> children = getChildrenContent(content, treeConfiguration); 168 169 boolean hasAtLeastOneAutoExpand = hasAutoExpandTargets(treeConfiguration); 170 171 List<Map<String, Object>> childrenInfos = new ArrayList<>(); 172 infos.put("children", childrenInfos); 173 174 for (String attributePath : children.keySet()) 175 { 176 for (Content childContent : children.get(attributePath)) 177 { 178 boolean expand = hasAtLeastOneAutoExpand && !isAnAutoExpandTarget(treeConfiguration, childContent); 179 180 Map<String, Object> childInfo = content2Json(childContent); 181 childInfo.put("metadataPath", attributePath); 182 childInfo.put("expanded", expand); 183 184 if (expand) 185 { 186 _addChildren(childContent, treeConfiguration, childInfo); 187 } 188 189 if (!hasChildrenContent(childContent, treeConfiguration)) 190 { 191 // childInfo.put("leaf", true); 192 childInfo.put("children", Collections.EMPTY_LIST); 193 } 194 else 195 { 196 childInfo.put("leaf", false); 197 childInfo.put("isExpanded", false); 198 } 199 200 childrenInfos.add(childInfo); 201 } 202 } 203 204 } 205 206 /** 207 * Get the path of children content which match filter regexp 208 * @param parentContentId The id of content to start search 209 * @param treeId The id of tree configuration 210 * @param value the value to match 211 * @return the matching paths composed by contents id separated by ';' 212 */ 213 @Callable 214 public List<String> filterChildrenContentByRegExp(String parentContentId, String treeId, String value) 215 { 216 List<String> matchingPaths = new ArrayList<>(); 217 218 Content parentContent = _ametysResolver.resolveById(parentContentId); 219 TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId); 220 221 String toMatch = StringUtils.stripAccents(value.toLowerCase()).trim(); 222 223 Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(parentContent, treeConfiguration); 224 for (List<Content> childrenContent : childrenContentByAttributes.values()) 225 { 226 for (Content childContent : childrenContent) 227 { 228 _getMatchingContents(childContent, toMatch, treeConfiguration, matchingPaths, parentContentId); 229 } 230 } 231 232 return matchingPaths; 233 } 234 235 private void _getMatchingContents(Content content, String value, TreeConfiguration treeConfiguration, List<String> matchingPaths, String parentPath) 236 { 237 if (isContentMatching(content, value)) 238 { 239 matchingPaths.add(parentPath + ";" + content.getId()); 240 } 241 242 Map<String, List<Content>> childrenContentByAttributes = getChildrenContent(content, treeConfiguration); 243 for (List<Content> childrenContent : childrenContentByAttributes.values()) 244 { 245 for (Content childContent : childrenContent) 246 { 247 _getMatchingContents(childContent, value, treeConfiguration, matchingPaths, parentPath + ";" + content.getId()); 248 } 249 } 250 } 251 252 /** 253 * Determines if content matches the filter regexp 254 * @param content the content 255 * @param value the value to match 256 * @return true if the content match 257 */ 258 protected boolean isContentMatching(Content content, String value) 259 { 260 String title = StringUtils.stripAccents(content.getTitle().toLowerCase()); 261 return title.contains(value); 262 } 263 264 /** 265 * Get the root node informations 266 * @param contentId The content 267 * @param treeId The contents tree id 268 * @return The informations 269 */ 270 @Callable 271 public Map<String, Object> getRootNodeInformations(String contentId, String treeId) 272 { 273 TreeConfiguration treeConfiguration = _getTreeConfiguration(treeId); 274 Content content = _ametysResolver.resolveById(contentId); 275 276 Map<String, Object> nodeInformations = content2Json(content); 277 _addChildren(content, treeConfiguration, nodeInformations); // auto expand first level + recursively if necessary 278 279 return nodeInformations; 280 } 281 282 /** 283 * Get the node informations 284 * @param contentId The content 285 * @return The informations 286 */ 287 @Callable 288 public Map<String, Object> getNodeInformations(String contentId) 289 { 290 Content content = _ametysResolver.resolveById(contentId); 291 return content2Json(content); 292 } 293 294 /** 295 * Get the default JSON representation of a content of the tree 296 * @param content the content 297 * @return the content as JSON 298 */ 299 protected Map<String, Object> content2Json(Content content) 300 { 301 Map<String, Object> infos = new HashMap<>(); 302 303 infos.put("id", "random-id-" + org.ametys.core.util.StringUtils.generateKey() + "-" + Math.round(Math.random() * 10_000)); 304 305 infos.put("contentId", content.getId()); 306 infos.put("contenttypesIds", content.getTypes()); 307 infos.put("name", content.getName()); 308 infos.put("title", content.getTitle()); 309 310 infos.put("iconGlyph", _contentTypesHelper.getIconGlyph(content)); 311 infos.put("iconDecorator", _contentTypesHelper.getIconDecorator(content)); 312 infos.put("iconSmall", _contentTypesHelper.getSmallIcon(content)); 313 infos.put("iconMedium", _contentTypesHelper.getMediumIcon(content)); 314 infos.put("iconLarge", _contentTypesHelper.getLargeIcon(content)); 315 316 return infos; 317 } 318 319 /** 320 * Get the default JSON representation of a child content 321 * @param content the content 322 * @param attributePath the path of attribute holding this content 323 * @return the content as JSON 324 */ 325 public Map<String, Object> childContent2Json(Content content, String attributePath) 326 { 327 Map<String, Object> childInfo = content2Json(content); 328 childInfo.put("metadataPath", attributePath); 329 return childInfo; 330 } 331 332 private void _merge(Map<String, List<Content>> childrenContent, Map<String, List<Content>> contents) 333 { 334 for (String key : contents.keySet()) 335 { 336 if (!childrenContent.containsKey(key)) 337 { 338 childrenContent.put(key, new ArrayList<>()); 339 } 340 341 List<Content> contentsList = childrenContent.get(key); 342 contentsList.addAll(contents.get(key)); 343 } 344 } 345 346 private Map<String, List<Content>> _handleAttributeTreeConfigurationElementsChild(ContentType contentType, ModelAwareDataHolder dataHolder, AttributeTreeConfigurationElementsChild attributeTreeConfigurationElementsChild, TreeConfiguration treeConfiguration) 347 { 348 Map<String, List<Content>> childrenContent = new HashMap<>(); 349 350 String attributePath = attributeTreeConfigurationElementsChild.getPath(); 351 352 try 353 { 354 Map<String, List<Content>> contents = _handleAttribute(contentType, dataHolder, attributePath); 355 _merge(childrenContent, contents); 356 } 357 catch (Exception e) 358 { 359 throw new IllegalArgumentException("An error occured on the tree configuration '" + treeConfiguration.getId() + "' getting for metadata '" + attributePath + "' on content type '" + contentType.getId() + "'", e); 360 } 361 362 return childrenContent; 363 } 364 365 private Map<String, List<Content>> _handleAttribute(ModelItemContainer modelItemContainer, ModelAwareDataHolder dataHolder, String attributePath) 366 { 367 Map<String, List<Content>> childrenContent = new HashMap<>(); 368 369 String currentModelItemName = StringUtils.substringBefore(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 370 371 ModelItem currentModelItem = modelItemContainer.getChild(currentModelItemName); 372 if (currentModelItem == null) 373 { 374 throw new IllegalArgumentException("No attribute definition for " + currentModelItemName); 375 } 376 377 378 if (dataHolder.hasValue(currentModelItemName)) 379 { 380 if (currentModelItem instanceof RepeaterDefinition) 381 { 382 ModelAwareRepeater repeater = dataHolder.getRepeater(currentModelItemName); 383 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 384 { 385 String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 386 Map<String, List<Content>> contents = _handleAttribute((RepeaterDefinition) currentModelItem, entry, subMetadataId); 387 388 _merge(childrenContent, contents); 389 } 390 } 391 else if (currentModelItem instanceof CompositeDefinition) 392 { 393 ModelAwareComposite metadata = dataHolder.getComposite(currentModelItemName); 394 395 String subMetadataId = StringUtils.substringAfter(attributePath, ModelItem.ITEM_PATH_SEPARATOR); 396 Map<String, List<Content>> contents = _handleAttribute((CompositeDefinition) currentModelItem, metadata, subMetadataId); 397 398 _merge(childrenContent, contents); 399 } 400 else if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(currentModelItem.getType().getId())) 401 { 402 ContentValue[] contentValues = dataHolder.getValue(currentModelItemName); 403 for (ContentValue contentValue : contentValues) 404 { 405 String key = currentModelItem.getPath(); 406 Optional<ModifiableContent> optContent = contentValue.getContentIfExists(); 407 if (optContent.isPresent()) 408 { 409 childrenContent.computeIfAbsent(key, k -> new ArrayList<>()).add(optContent.get()); 410 } 411 else 412 { 413 getLogger().warn("On a data holder, the attribute '{}' is referencing a unexisting content: '{}'", key, contentValue.getContentId()); 414 } 415 } 416 } 417 else 418 { 419 throw new IllegalArgumentException("The metadata definition for " + currentModelItem.getPath() + " is not a content"); 420 } 421 } 422 423 return childrenContent; 424 } 425 426 /** 427 * Get the tree configuration 428 * @param treeId the tree id 429 * @return the tree configuration 430 */ 431 protected TreeConfiguration _getTreeConfiguration(String treeId) 432 { 433 if (StringUtils.isBlank(treeId)) 434 { 435 throw new IllegalArgumentException("The tree information cannot be obtain, because 'tree' is null"); 436 } 437 438 TreeConfiguration treeConfiguration = _treeExtensionPoint.getExtension(treeId); 439 if (treeConfiguration == null) 440 { 441 throw new IllegalArgumentException("There is no tree configuration for '" + treeId + "'"); 442 } 443 return treeConfiguration; 444 } 445 446 /** 447 * Get the parent content of a tree 448 * @param parentId the parent id 449 * @return the parent content of a tree 450 * @throws IllegalArgumentException if an exception occurred 451 */ 452 protected Content _getParentContent(String parentId) throws IllegalArgumentException 453 { 454 if (StringUtils.isBlank(parentId)) 455 { 456 throw new IllegalArgumentException("The tree information cannot be obtain, because 'node' is null"); 457 } 458 459 try 460 { 461 return _ametysResolver.resolveById(parentId); 462 } 463 catch (Exception e) 464 { 465 throw new IllegalArgumentException("The tree configuration cannot be used on an object that is not a content: " + parentId, e); 466 } 467 468 } 469 470 /** 471 * Should auto expand until some kind of node? 472 * @param treeConfiguration The tree configuration 473 * @return true if should auto expand 474 */ 475 protected boolean hasAutoExpandTargets(TreeConfiguration treeConfiguration) 476 { 477 return treeConfiguration.getElements() 478 .stream() 479 .map(TreeConfigurationElements::getContentTypesConfiguration) 480 .flatMap(Collection::stream) 481 .anyMatch(TreeConfigurationContentType::autoExpandTarget); 482 } 483 484 /** 485 * Should auto expand until some kind of node? 486 * @param treeConfiguration The tree configuration 487 * @param content The content involved 488 * @return true if should auto expand to it 489 */ 490 protected boolean isAnAutoExpandTarget(TreeConfiguration treeConfiguration, Content content) 491 { 492 List<String> contentTypes = Arrays.asList(content.getTypes()); 493 494 return treeConfiguration.getElements() 495 .stream() 496 .map(TreeConfigurationElements::getContentTypesConfiguration) 497 .flatMap(Collection::stream) 498 .filter(TreeConfigurationContentType::autoExpandTarget) 499 .map(TreeConfigurationContentType::getContentTypesIds) 500 .anyMatch(ct -> { Set<String> hs = new HashSet<>(ct); hs.retainAll(contentTypes); return hs.size() > 0; }); 501 } 502}