001/* 002 * Copyright 2025 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.model.properties; 017 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Objects; 021import java.util.stream.Stream; 022 023import org.apache.avalon.framework.configuration.Configuration; 024import org.apache.avalon.framework.configuration.ConfigurationException; 025import org.apache.commons.lang3.StringUtils; 026 027import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject; 028import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 029import org.ametys.runtime.model.ElementDefinition; 030import org.ametys.runtime.model.Model; 031import org.ametys.runtime.model.type.ModelItemTypeConstants; 032 033/** 034 * <p>{@link Property} getting the first available value as string, supporting concatenation with default separator '-'.</p> 035 * 036 * <p> 037 * For example, with this configuration: 038 * <code> 039 * <pre> 040 * <items> 041 * <item ref="myNumericCode"/> 042 * </items> 043 * <items> 044 * <item ref="otherCode"/> 045 * <item ref="linkedContent/code"/> 046 * </items> 047 * <items separator="/"> 048 * <item ref="alternativeCode"/> 049 * <item ref="linkedContent/code" optional="true"/> 050 * </items> 051 * </pre> 052 * </code> 053 * We will first search for "myNumericCode".<br/> 054 * If empty, we will search for concatenation of "otherCode" and the "code" item on "linkedContent".<br/> 055 * If empty, we will search for concatenation of "alternativeCode" and the "code" item on "linkedContent", even if this one is empty due to the optional attribute.<br/> 056 * The first non-empty value is returned.<br/> 057 * Otherwise, null is returned. 058 * </p> 059 * 060 * <p>Referenced items have to be single. If a part of the path is multiple (repeater, multiple references, etc.), it will be rejected by a {@link ConfigurationException}.</p> 061 * 062 * @param <X> Type of ametys object supported by this property 063 */ 064public class ComposedSingleModelItemsProperty<X extends ModelAwareDataAwareAmetysObject> extends AbstractIndexableStaticProperty<String, String, X> 065{ 066 private static final String __DEFAULT_SEPARATOR = "-"; 067 068 private List<ComposedString> _composedStrings; 069 070 @Override 071 public void configure(Configuration configuration) throws ConfigurationException 072 { 073 super.configure(configuration); 074 075 _composedStrings = Stream.of(configuration.getChildren("items")) 076 .map( 077 items -> 078 new ComposedString( 079 Stream.of(items.getChildren("item")) 080 .sequential() 081 .map(item -> new ItemRefDefinition(item.getAttribute("ref", StringUtils.EMPTY), item.getAttributeAsBoolean("optional", false))) 082 .toList(), 083 items.getAttribute("separator", __DEFAULT_SEPARATOR) 084 ) 085 ) 086 .toList(); 087 } 088 089 @Override 090 public void init(String availableTypesRole) throws Exception 091 { 092 Model model = getModel(); 093 094 List<ComposedString> keptComposedStrings = new ArrayList<>(_composedStrings); 095 096 for (ComposedString composedString : _composedStrings) 097 { 098 for (ItemRefDefinition referenceDefinition : composedString.itemRefs()) 099 { 100 String reference = referenceDefinition.ref(); 101 102 // If the model item does not exist, remove the composed string and log a warning 103 if (!model.hasModelItem(reference)) 104 { 105 _logger.warn("'" + reference + "' is invalid for property '" + getName() + "' in model '" + model.getId() + "'. The referenced item has not been found."); 106 keptComposedStrings.remove(composedString); 107 break; 108 } 109 110 // The model item has to be a final element 111 if (!(model.getModelItem(reference) instanceof ElementDefinition)) 112 { 113 throw new ConfigurationException("'" + reference + "' is invalid for property '" + getName() + "' in model '" + model.getId() + "'. The referenced item is a group."); 114 } 115 116 // The model item has to be single in its whole path (no multiple value, no repeater, no multiple links) 117 if (DataHolderHelper.isMultiple(model, reference)) 118 { 119 throw new ConfigurationException("'" + reference + "' is invalid for property '" + getName() + "' in model '" + model.getId() + "'. The referenced item is multiple."); 120 } 121 } 122 } 123 124 _composedStrings = keptComposedStrings; 125 126 super.init(availableTypesRole); 127 } 128 129 public Object getValue(X dataHolder) 130 { 131 return _composedStrings.stream() 132 .sequential() 133 .map(composedString -> _buildComposedString(dataHolder, composedString)) 134 .filter(Objects::nonNull) 135 .findFirst() 136 .orElse(null); 137 } 138 139 private String _buildComposedString(X dataHolder, ComposedString composedString) 140 { 141 List<String> itemValues = new ArrayList<>(); 142 for (ItemRefDefinition itemRef : composedString.itemRefs()) 143 { 144 if (dataHolder.hasValue(itemRef.ref())) 145 { 146 itemValues.add(dataHolder.getValue(itemRef.ref()).toString()); 147 } 148 // If item ref is not optional, composed string is invalid, check the next one 149 else if (!itemRef.optional()) 150 { 151 return null; 152 } 153 } 154 155 return itemValues.isEmpty() ? null : StringUtils.join(itemValues, composedString.separator()); 156 } 157 158 @Override 159 public boolean isMultiple() 160 { 161 return false; 162 } 163 164 @Override 165 protected String getTypeId() 166 { 167 return ModelItemTypeConstants.STRING_TYPE_ID; 168 } 169 170 private record ComposedString(List<ItemRefDefinition> itemRefs, String separator) { /* empty */ } 171 private record ItemRefDefinition(String ref, Boolean optional) { /* empty */ } 172}