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}