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