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