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}