001/*
002 *  Copyright 2023 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.ui.model;
017
018import java.util.HashMap;
019import java.util.Map;
020import java.util.Optional;
021
022import org.apache.commons.lang3.StringUtils;
023
024import org.ametys.cms.data.type.ModelItemTypeConstants;
025import org.ametys.cms.data.type.indexing.SortableIndexableElementType;
026import org.ametys.cms.model.properties.Property;
027import org.ametys.cms.search.model.IndexationAwareElementDefinition;
028import org.ametys.cms.search.model.SystemProperty;
029import org.ametys.cms.search.ui.model.impl.RepeaterSearchUIColumn;
030import org.ametys.cms.search.ui.model.impl.ViewElementAccessorSearchUIColumn;
031import org.ametys.cms.search.ui.model.impl.ViewElementSearchUIColumn;
032import org.ametys.plugins.repository.model.RepeaterDefinition;
033import org.ametys.runtime.model.ElementDefinition;
034import org.ametys.runtime.model.ModelHelper;
035import org.ametys.runtime.model.ModelItem;
036import org.ametys.runtime.model.ModelItemAccessor;
037import org.ametys.runtime.model.ModelItemContainer;
038import org.ametys.runtime.model.ModelViewItem;
039import org.ametys.runtime.model.ViewItem;
040import org.ametys.runtime.model.ViewItemAccessor;
041
042/**
043 * Helper class for search UI columns
044 */
045public final class SearchUIColumnHelper
046{
047    /** The default column width */
048    public static final int DEFAULT_COLUMN_WIDTH = 200;
049    /** The default column width for system properties */
050    public static final int DEFAULT_SYSTEM_PROPERTY_COLUMN_WIDTH = 150;
051    
052    private SearchUIColumnHelper()
053    {
054        // Empty constructor
055    }
056    
057    /**
058     * Creates a column for the given model item
059     * @param modelItem the model item
060     * @return the created column
061     * @throws IllegalArgumentException if the given model item is not an element or a repeater
062     */
063    public static SearchUIColumn createModelItemColumn(ModelItem modelItem) throws IllegalArgumentException
064    {
065        SearchUIColumn column;
066        if (modelItem instanceof RepeaterDefinition repeaterDefinition)
067        {
068            column = new RepeaterSearchUIColumn();
069            ((RepeaterSearchUIColumn) column).setDefinition(repeaterDefinition);
070        }
071        else if (modelItem instanceof ElementDefinition elementDefinition)
072        {
073            if (modelItem instanceof ModelItemAccessor)
074            {
075                column = new ViewElementAccessorSearchUIColumn();
076                ((ViewElementAccessorSearchUIColumn) column).setDefinition(elementDefinition);
077            }
078            else
079            {
080                column = new ViewElementSearchUIColumn();
081                ((ViewElementSearchUIColumn) column).setDefinition(elementDefinition);
082            }
083        }
084        else
085        {
086            throw new IllegalArgumentException("Unable to create a column from the given model item '" + modelItem.getPath() + "'. This model item is not a repeater or an element.");
087        }
088        
089        return column;
090    }
091    
092    /**
093     * Retrieves the default column width, corresponding to the referenced model item
094     * @param column the column
095     * @return the default width
096     */
097    public static int getDefaultColumnWidth(SearchUIColumn column)
098    {
099        if (column.getDefinition() instanceof SystemProperty systemProperty)
100        {
101            return Optional.ofNullable(systemProperty.getColumnWidth())
102                    .orElse(DEFAULT_SYSTEM_PROPERTY_COLUMN_WIDTH);
103        }
104        else
105        {
106            return DEFAULT_COLUMN_WIDTH;
107        }
108    }
109    
110    /**
111     * Determines if the inline edition is allowed for the given column
112     * @param column the column
113     * @return <code>true</code> if the column edition is allowed, <code>false</code> otherwise
114     */
115    public static boolean isEditionAllowed(SearchUIColumn column)
116    {
117        if (_isMultiLevelMultiple(column))
118        {
119            // column is not editable if it references a model item with a multiple parent
120            return false;
121        }
122        
123        ModelItem modelItem = column.getDefinition();
124        if (modelItem instanceof ElementDefinition definition && !definition.isEditable())
125        {
126            return false;
127        }
128        
129        if (_isJoinedModelItem(column))
130        {
131            // column is not editable if it is on a distant content
132            return false;
133        }
134        
135        if (ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(modelItem.getType().getId()))
136        {
137            // richtext are never editable inline
138            return false;
139        }
140        
141        if (org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID.equals(modelItem.getType().getId()))
142        {
143            // disallow edition for repeaters containing richtexts
144            return !ModelHelper.hasModelItemOfType((ModelItemContainer) modelItem, ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID);
145        }
146        
147        return true;
148    }
149    
150    /**
151     * Check if the column references a model item of a distant content type
152     * @param column the column
153     * @return <code>true</code> if the column references a model item of a distant content type, <code>false</code> otherwise
154     */
155    private static boolean _isJoinedModelItem(SearchUIColumn column)
156    {
157        ViewItemAccessor parent = column.getParent();
158        while (parent != null)
159        {
160            if (parent instanceof ModelViewItem parentModelViewItem && ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(parentModelViewItem.getDefinition().getType().getId()))
161            {
162                // One of the parent level is a join (link on a distant content)
163                return true;
164            }
165            
166            parent = parent instanceof ViewItem parentViewItem ? parentViewItem.getParent() : null;
167        }
168        
169        // No parent is a join
170        return false;
171    }
172    
173
174    
175    /**
176     * Determines if the sort is allowed for the given column
177     * @param column the column
178     * @return <code>true</code> if the column edition is allowed, <code>false</code> otherwise
179     */
180    public static boolean isSortAllowed(SearchUIColumn column)
181    {
182        return (column.allowSortOnMultipleJoin() || !_isMultiLevelMultiple(column))
183                && _isModelItemSortable(column.getDefinition());
184    }
185    
186    /**
187     * Check if the column references a model item with a multiple parent
188     * @param column the column
189     * @return <code>true</code> if the column references a model item with a multiple parent, <code>false</code> otherwise
190     */
191    private static boolean _isMultiLevelMultiple(SearchUIColumn column)
192    {
193        ViewItemAccessor parent = column.getParent();
194        while (parent != null && !(parent instanceof SearchUIColumn))
195        {
196            if (parent instanceof ModelViewItem parentModelViewItem && _isModelItemMultiple(parentModelViewItem.getDefinition()))
197            {
198                // One of the parent level is multiple
199                return true;
200            }
201            
202            parent = parent instanceof ViewItem parentViewItem ? parentViewItem.getParent() : null;
203        }
204        
205        // No parent is multiple
206        return false;
207    }
208    
209    /**
210     * Check if the given model item is multiple
211     * @param modelItem the model item
212     * @return <code>true</code> if the model item is multiple, <code>false</code> otherwise
213     */
214    private static boolean _isModelItemMultiple(ModelItem modelItem)
215    {
216        return modelItem instanceof ElementDefinition && ((ElementDefinition) modelItem).isMultiple()
217                || modelItem instanceof RepeaterDefinition;
218    }
219    
220    /**
221     * Determines if the column is sortable according its model item
222     * @param modelItem the model item
223     * @return <code>true</code> if model item is sortable, <code>false</code> otherwise
224     */
225    private static boolean _isModelItemSortable(ModelItem modelItem)
226    {
227        return modelItem instanceof IndexationAwareElementDefinition indexationAwareElementDefinition
228            ? indexationAwareElementDefinition.isSortable()
229            : modelItem instanceof Property
230                ? false
231                : modelItem.getType() instanceof SortableIndexableElementType;
232    }
233    
234    /**
235     * Retrieves the default converter for the given definition
236     * @param definition the definition
237     * @return the default converter
238     */
239    public static Optional<String> getElementDefinitionDefaultConverter(ElementDefinition definition)
240    {
241        String defaultConverter = null;
242        if (definition instanceof SystemProperty systemProperty)
243        {
244            defaultConverter = systemProperty.getConverter();
245        }
246        
247        return Optional.ofNullable(defaultConverter)
248                       .filter(StringUtils::isNotEmpty);
249    }
250    
251    /**
252     * Converts the given column's properties in a JSON map
253     * @param column the column
254     * @return The column's properties as a JSON map
255     */
256    public static Map<String, Object> columnPropertiesToJSON(SearchUIColumn column)
257    {
258        Map<String, Object> json = new HashMap<>();
259        
260        json.put("path", _getRelativePathToFirstParentColumn(column));
261        json.put("width", column.getWidth());
262        json.put("hidden", column.isHidden());
263        json.put("renderer", column.getRenderer());
264        json.put("converter", column.getConverter());
265        json.put("editable", column.isEditable());
266        json.put("sortable", column.isSortable());
267        json.put("defaultSorter", column.getDefaultSorter());
268        
269        json.put("multiple", _isModelItemMultiple(column.getDefinition()) || _isMultiLevelMultiple(column));
270        
271        return json;
272    }
273    
274    private static String _getRelativePathToFirstParentColumn(SearchUIColumn column)
275    {
276        String path = column.getName();
277        ViewItemAccessor parent = column.getParent();
278        while (parent != null && !(parent instanceof SearchUIColumn))
279        {
280            if (parent instanceof ModelViewItem parentModelViewItem)
281            {
282                path = parentModelViewItem.getName() + ModelItem.ITEM_PATH_SEPARATOR + path;
283            }
284            
285            parent = parent instanceof ViewItem parentViewItem ? parentViewItem.getParent() : null;
286        }
287        
288        return path;
289    }
290}