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