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