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