001/* 002 * Copyright 2020 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.search.cocoon; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.context.Context; 027import org.apache.avalon.framework.context.ContextException; 028import org.apache.avalon.framework.context.Contextualizable; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.ProcessingException; 033import org.apache.cocoon.components.ContextHelper; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.content.ContentHelper; 037import org.ametys.cms.contenttype.ContentType; 038import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 039import org.ametys.cms.contenttype.ContentTypesHelper; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.search.content.ContentValuesExtractorFactory; 042import org.ametys.cms.search.content.ContentValuesExtractorFactory.ContentValuesExtractor; 043import org.ametys.cms.search.content.ContentValuesExtractorFactory.SimpleContentValuesExtractor; 044import org.ametys.cms.search.model.SystemProperty; 045import org.ametys.cms.search.ui.model.ColumnHelper; 046import org.ametys.cms.search.ui.model.SearchUIColumn; 047import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 048import org.ametys.cms.search.ui.model.impl.RepeaterSearchUIColumn; 049import org.ametys.core.right.RightManager; 050import org.ametys.core.ui.Callable; 051import org.ametys.core.util.ServerCommHelper; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.repository.data.type.ModelItemTypeConstants; 054import org.ametys.plugins.repository.model.CompositeDefinition; 055import org.ametys.runtime.model.DefinitionContext; 056import org.ametys.runtime.model.ModelViewItemGroup; 057import org.ametys.runtime.model.SimpleViewItemGroup; 058import org.ametys.runtime.model.View; 059import org.ametys.runtime.model.ViewHelper; 060import org.ametys.runtime.model.ViewItem; 061import org.ametys.runtime.model.ViewItemAccessor; 062 063/** 064 * Generates the columns information for the grid based upon a view of a contenttype 065 */ 066public class ContentGridComponent implements Contextualizable, Serviceable, Component 067{ 068 /** The avalon role */ 069 public static final String ROLE = ContentGridComponent.class.getName(); 070 071 private static final String __REPEATERS_DEFAULT_EXTERNAL_DISABLE_CONDITION_VALUES = "__repeatersDefaultExternalDisableConditionValues"; 072 073 /** The servercomm helper */ 074 protected ServerCommHelper _serverCommHelper; 075 /** The Ametys object resolver */ 076 protected AmetysObjectResolver _resolver; 077 /** The contenttypes extension point */ 078 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 079 /** Cocoon context */ 080 protected Context _context; 081 /** The helper for columns */ 082 protected ColumnHelper _columnHelper; 083 /** The service manager */ 084 protected ServiceManager _manager; 085 /** The ContentValuesExtractorFactory */ 086 protected ContentValuesExtractorFactory _contentValuesExtractorFactory; 087 /** The AmetysObjectResolver instance */ 088 protected AmetysObjectResolver _ametysObjectResolver; 089 /** The ContentTypesHelper instance */ 090 protected ContentTypesHelper _contentTypesHelper; 091 /** The ContentHelper instance */ 092 protected ContentHelper _contentHelper; 093 /** The right manager */ 094 protected RightManager _rightManager; 095 096 @Override 097 public void service(ServiceManager manager) throws ServiceException 098 { 099 _manager = manager; 100 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 101 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 102 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 103 _serverCommHelper = (ServerCommHelper) manager.lookup(ServerCommHelper.ROLE); 104 _columnHelper = (ColumnHelper) manager.lookup(ColumnHelper.ROLE); 105 _contentValuesExtractorFactory = (ContentValuesExtractorFactory) manager.lookup(ContentValuesExtractorFactory.ROLE); 106 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 107 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 108 } 109 110 public void contextualize(Context context) throws ContextException 111 { 112 _context = context; 113 } 114 115 /** 116 * Generates the columns informations for a View of a ContentType 117 * @param contentIds The contents identifiers 118 * @param contentTypeId The content type id. Mandatory. 119 * @param viewName The view of the content type. 120 * @return The columns informations 121 * @throws IllegalArgumentException If one argument is not as expected 122 * @throws IllegalStateException If an error occurred while processing the columns 123 * @throws ProcessingException if an error occurs while generating columns infos 124 */ 125 @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION) 126 public Map<String, Object> getColumnsAndValues(List<String> contentIds, String contentTypeId, String viewName) throws IllegalStateException, IllegalArgumentException, ProcessingException 127 { 128 ContentType contentType = _getContentType(contentTypeId); 129 View editionView = _getEditionView(contentType, viewName); 130 View copyWithColumns = new View(); 131 editionView.copyTo(copyWithColumns); 132 copyWithColumns.addViewItems(_copyViewItemsAsColumns(editionView)); 133 134 List<Map<String, Object>> columnsInfo = _getColumnsInfo(copyWithColumns); 135 136 Map<String, Object> results = new HashMap<>(); 137 results.put("columns", columnsInfo); 138 results.put("contentType", contentType.getId()); 139 results.put("view", copyWithColumns.getName()); 140 141 SimpleContentValuesExtractor contentValuesExtractor = _contentValuesExtractorFactory.create(copyWithColumns); 142 Map objectModel = ContextHelper.getObjectModel(_context); 143 Locale defaultLocale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 144 145 List<Map<String, Object>> contents = new ArrayList<>(); 146 List<String> noRightContents = new ArrayList<>(); 147 for (String contentId : contentIds) 148 { 149 Content content = _ametysObjectResolver.resolveById(contentId); 150 if (_rightManager.currentUserHasReadAccess(content)) 151 { 152 Map<String, Object> properties = getContentData(content, contentValuesExtractor, defaultLocale, Collections.emptyMap()); 153 contents.add(properties); 154 } 155 else 156 { 157 noRightContents.add(contentId); 158 } 159 } 160 results.put("contents", contents); 161 results.put("noright-contents", noRightContents); 162 163 return results; 164 } 165 166 private List<ViewItem> _copyViewItemsAsColumns(ViewItemAccessor original) 167 { 168 List<ViewItem> viewItems = new ArrayList<>(); 169 170 org.ametys.plugins.repository.model.ViewHelper.visitView(original, 171 (element, definition) -> { 172 // simple element 173 if (definition instanceof SystemProperty systemProperty && !systemProperty.isDisplayable()) 174 { 175 throw new IllegalArgumentException("The system property '" + systemProperty.getName() + "' is not displayable."); 176 } 177 178 SearchUIColumn searchUIColumn = SearchUIColumnHelper.createModelItemColumn(definition); 179 element.copyTo(searchUIColumn); 180 181 viewItems.add(searchUIColumn); 182 }, 183 (group, definition) -> { 184 // composite 185 ModelViewItemGroup<CompositeDefinition> groupCopy = new ModelViewItemGroup<>(); 186 group.copyTo(groupCopy); 187 188 groupCopy.addViewItems(_copyViewItemsAsColumns(group)); 189 190 viewItems.add(groupCopy); 191 }, 192 (group, definition) -> { 193 // repeater 194 RepeaterSearchUIColumn searchUIColumn = new RepeaterSearchUIColumn(); 195 group.copyTo(searchUIColumn); 196 197 searchUIColumn.addViewItems(_copyViewItemsAsColumns(group)); 198 199 viewItems.add(searchUIColumn); 200 }, 201 group -> { 202 // simple view item group 203 SimpleViewItemGroup groupCopy = new SimpleViewItemGroup(); 204 group.copyTo(groupCopy); 205 206 groupCopy.addViewItems(_copyViewItemsAsColumns(group)); 207 208 viewItems.add(groupCopy); 209 }); 210 211 return viewItems; 212 } 213 214 private List<Map<String, Object>> _getColumnsInfo(ViewItemAccessor viewItemAccessor) throws ProcessingException 215 { 216 List<Map<String, Object>> jsonObject = new ArrayList<>(); 217 218 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 219 { 220 if (viewItem instanceof SearchUIColumn searchUIColumn) 221 { 222 Map<String, Object> columnInfo = searchUIColumn.toJSON(DefinitionContext.newInstance()); 223 columnInfo.put("field", searchUIColumn.getDefinition().getPath()); 224 jsonObject.add(columnInfo); 225 } 226 else if (viewItem instanceof ViewItemAccessor itemAccessor) 227 { 228 jsonObject.addAll(_getColumnsInfo(itemAccessor)); 229 } 230 } 231 232 return jsonObject; 233 } 234 235 private ContentType _getContentType(String contentTypeId) throws IllegalArgumentException 236 { 237 if (contentTypeId.isBlank()) 238 { 239 throw new IllegalArgumentException("The contentType argument is mandatory"); 240 } 241 242 ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId); 243 if (contentType == null) 244 { 245 throw new IllegalArgumentException("The content type '" + contentTypeId + "' specified does not exist"); 246 } 247 248 return contentType; 249 } 250 251 private View _getEditionView(ContentType contentType, String viewName) throws IllegalArgumentException 252 { 253 View view; 254 if (StringUtils.isBlank(viewName)) 255 { 256 view = contentType.getView("default-edition"); 257 if (view == null) 258 { 259 view = contentType.getView("main"); 260 if (view == null) 261 { 262 throw new IllegalArgumentException("The content type '" + contentType.getId() + "' has no 'default-edition' nor 'main' view. Specify a view to use"); 263 } 264 } 265 } 266 else 267 { 268 view = contentType.getView(viewName); 269 if (view == null) 270 { 271 throw new IllegalArgumentException("The content type '" + contentType.getId() + "' has no '" + viewName + "' view"); 272 } 273 } 274 275 return ViewHelper.getTruncatedView(view); 276 } 277 278 /** 279 * Generate standard content data. 280 * @param content The content. 281 * @param extractor The content values extractor which generates. 282 * @param defaultLocale the default locale for localized values if content's language is null. 283 * @param contextualParameters The search contextual parameters. 284 * @return A Map containing the content data. 285 */ 286 public Map<String, Object> getContentData(Content content, ContentValuesExtractor extractor, Locale defaultLocale, Map<String, Object> contextualParameters) 287 { 288 Map<String, Object> contentData = new HashMap<>(); 289 290 contentData.put("id", content.getId()); 291 contentData.put("name", content.getName()); 292 293 if (_contentHelper.isMultilingual(content) && ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getDefinition("title").getType().getId())) 294 { 295 contentData.put("title", _contentHelper.getTitleVariants(content)); 296 } 297 else 298 { 299 contentData.put("title", _contentHelper.getTitle(content)); 300 } 301 302 contentData.put("language", content.getLanguage()); 303 contentData.put("contentTypes", content.getTypes()); 304 contentData.put("mixins", content.getMixinTypes()); 305 contentData.put("iconGlyph", _contentTypesHelper.getIconGlyph(content)); 306 contentData.put("iconDecorator", _contentTypesHelper.getIconDecorator(content)); 307 contentData.put("smallIcon", _contentTypesHelper.getSmallIcon(content)); 308 contentData.put("mediumIcon", _contentTypesHelper.getMediumIcon(content)); 309 contentData.put("largeIcon", _contentTypesHelper.getLargeIcon(content)); 310 contentData.put("isSimple", _contentHelper.isSimple(content)); 311 contentData.put("archived", _contentHelper.isArchivedContent(content)); 312 313 contentData.put("properties", extractor.getValues(content, defaultLocale, contextualParameters)); 314 contentData.put(__REPEATERS_DEFAULT_EXTERNAL_DISABLE_CONDITION_VALUES, extractor.getRepeatersDefaultExternalDisableConditionValues(content, contextualParameters)); 315 316 return contentData; 317 } 318}