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