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}