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