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 * &lt;items>
041 *     &lt;item ref="myNumericCode"/>
042 * &lt;/items>
043 * &lt;items>
044 *     &lt;item ref="otherCode"/>
045 *     &lt;item ref="linkedContent/code"/>
046 * &lt;/items>
047 * &lt;items separator="/">
048 *     &lt;item ref="alternativeCode"/>
049 *     &lt;item ref="linkedContent/code" optional="true"/>
050 * &lt;/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}