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        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        for (String contentId : contentIds)
147        {
148            Content content = _ametysObjectResolver.resolveById(contentId);
149            Map<String, Object> properties = getContentData(content, contentValuesExtractor, defaultLocale, Collections.emptyMap());
150            contents.add(properties);
151        }
152        results.put("contents", contents);
153        
154        return results;
155    }
156    
157    private List<Map<String, Object>> _generateColumns(List<ViewItem> viewItems, ContentType contentType) throws ProcessingException
158    {
159        List<Map<String, Object>> columns = new ArrayList<>();
160        
161        for (ViewItem viewItem : viewItems)
162        {
163            if (viewItem instanceof ViewItemGroup && !(viewItem instanceof ModelViewItemGroup)
164                    || viewItem instanceof ModelViewItemGroup && ((ModelViewItemGroup) viewItem).getDefinition() instanceof CompositeDefinition)
165            {
166                // ViewItemGroup and Composite
167                columns.addAll(_generateColumns(((ViewItemGroup) viewItem).getViewItems(), contentType));
168            }
169            else
170            {
171                // Repeater or Attributes
172                ModelViewItem modelViewItem = (ModelViewItem) viewItem;
173                ModelItem definition = modelViewItem.getDefinition();
174                columns.add(_definitionToColumnJSON(definition, contentType));
175            }
176        }
177        
178        return columns;
179    }
180    
181    private Map<String, Object> _definitionToColumnJSON(ModelItem definition, ContentType contentType) throws ProcessingException
182    {
183        SearchUIColumn searchUIColumn = new MetadataSearchUIColumn();
184        try
185        {
186            Logger logger = LoggerFactory.getLogger(MetadataSearchUIColumn.class);
187            Configuration columnConfig = _getColumnConfiguration(definition, contentType);
188            LifecycleHelper.setupComponent(searchUIColumn, new SLF4JLoggerAdapter(logger), _context, _manager, columnConfig);
189            
190            Map<String, Object> columnInfo = _searchUIModelHelper.getColumnInfo(searchUIColumn);
191            columnInfo.put("field", definition.getPath());
192            return columnInfo;
193        }
194        catch (Exception e)
195        {
196            throw new ProcessingException("Unable to initialize search ui column for attribute '" + definition.getPath() + "'.", e);
197        }
198        finally 
199        {
200            LifecycleHelper.dispose(searchUIColumn);
201        }
202    }
203
204    private Configuration _getColumnConfiguration(ModelItem modelItem, ContentType contentType)
205    {
206        DefaultConfiguration columnConfig = new DefaultConfiguration("column");
207        
208        DefaultConfiguration modelItemConfig = new DefaultConfiguration("metadata");
209        modelItemConfig.setAttribute("path", modelItem.getPath());
210        columnConfig.addChild(modelItemConfig);
211        
212        DefaultConfiguration contentTypesConfig = new DefaultConfiguration("contentTypes");
213        DefaultConfiguration baseCTypeConfig = new DefaultConfiguration("baseType");
214        baseCTypeConfig.setAttribute("id", modelItem.getModel().getId());
215        contentTypesConfig.addChild(baseCTypeConfig);
216        
217        DefaultConfiguration cTypeConfig = new DefaultConfiguration("type");
218        cTypeConfig.setAttribute("id", contentType.getId());
219        contentTypesConfig.addChild(cTypeConfig);
220        columnConfig.addChild(contentTypesConfig);
221        
222        return columnConfig;
223    }
224
225    private ContentType _getContentType(String contentTypeId) throws IllegalArgumentException
226    {
227        if (contentTypeId.isBlank())
228        {
229            throw new IllegalArgumentException("The contentType argument is mandatory");
230        }
231        
232        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
233        if (contentType == null)
234        {
235            throw new IllegalArgumentException("The content type '" + contentTypeId +  "' specified does not exist");
236        }
237        
238        return contentType;
239    }
240    
241    private View _getEditionView(ContentType contentType, String viewName) throws IllegalArgumentException
242    {
243        View view;
244        if (StringUtils.isBlank(viewName))
245        {
246            view = contentType.getView("default-edition");
247            if (view == null)
248            {
249                view = contentType.getView("main");
250                if (view == null)
251                {
252                    throw new IllegalArgumentException("The content type '" + contentType.getId() + "' has no 'default-edition' nor 'main' view. Specify a view to use");
253                }
254            }
255        }
256        else
257        {
258            view = contentType.getView(viewName);
259            if (view == null)
260            {
261                throw new IllegalArgumentException("The content type '" + contentType.getId() + "' has no '" + viewName + "' view");
262            }
263        }
264        
265        return ViewHelper.getTruncatedView(view);
266    }
267    
268    /**
269     * Generate standard content data.
270     * @param content The content.
271     * @param extractor The content values extractor which generates.
272     * @param defaultLocale the default locale for localized values if content's language is null. 
273     * @param contextualParameters The search contextual parameters.
274     * @return A Map containing the content data.
275     */
276    public Map<String, Object> getContentData(Content content, ContentValuesExtractor extractor, Locale defaultLocale, Map<String, Object> contextualParameters)
277    {
278        Map<String, Object> contentData = new HashMap<>();
279        
280        contentData.put("id", content.getId());
281        contentData.put("name", content.getName());
282        
283        if (_contentHelper.isMultilingual(content) && ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getDefinition("title").getType().getId()))
284        {
285            contentData.put("title", _contentHelper.getTitleVariants(content));
286        }
287        else
288        {
289            contentData.put("title", _contentHelper.getTitle(content));
290        }
291        
292        contentData.put("language", content.getLanguage());
293        contentData.put("contentTypes", content.getTypes());
294        contentData.put("mixins", content.getMixinTypes());
295        contentData.put("iconGlyph", _contentTypesHelper.getIconGlyph(content));
296        contentData.put("iconDecorator", _contentTypesHelper.getIconDecorator(content));
297        contentData.put("smallIcon", _contentTypesHelper.getSmallIcon(content));
298        contentData.put("mediumIcon", _contentTypesHelper.getMediumIcon(content));
299        contentData.put("largeIcon", _contentTypesHelper.getLargeIcon(content));
300        contentData.put("isSimple", _contentHelper.isSimple(content));
301        contentData.put("archived", _contentHelper.isArchivedContent(content));
302        
303        contentData.put("properties", extractor.getValues(content, defaultLocale, contextualParameters));
304        
305        return contentData;
306    }
307}