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