001/*
002 *  Copyright 2026 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 */
016
017package org.ametys.plugins.ai.rest;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.environment.Request;
029import org.apache.commons.lang3.Strings;
030
031import org.ametys.cms.data.type.ModelItemTypeConstants;
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.search.SearchResults;
034import org.ametys.cms.search.content.ContentSearcherFactory;
035import org.ametys.cms.search.model.SearchModelCriterionDefinition;
036import org.ametys.cms.search.model.impl.ReferencingSearchModelCriterionDefinition;
037import org.ametys.cms.search.query.MatchAllQuery;
038import org.ametys.cms.search.ui.model.SearchModelCriterionViewItem;
039import org.ametys.cms.search.ui.model.SearchUIColumn;
040import org.ametys.cms.search.ui.model.SearchUIModel;
041import org.ametys.core.cocoon.JSonReader;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.plugins.repository.UnknownAmetysObjectException;
044import org.ametys.runtime.model.ElementDefinition;
045import org.ametys.runtime.model.ModelItem;
046import org.ametys.runtime.model.ModelViewItem;
047import org.ametys.runtime.model.ViewElement;
048import org.ametys.runtime.model.ViewItem;
049import org.ametys.runtime.model.ViewItemAccessor;
050import org.ametys.runtime.model.ViewItemContainer;
051import org.ametys.runtime.model.type.DataContext;
052
053/**
054 * The Search Web Service to get configuration with enums values.
055 */
056public class GetSearchModelConfigurationAction extends AbstractSearchAction
057{
058    /** The content searcher factory */
059    protected ContentSearcherFactory _contentSearcherFactory;
060    
061    /** The Ametys object resolver */
062    protected AmetysObjectResolver _resolver;
063    
064    @Override
065    public void service(ServiceManager smanager) throws ServiceException
066    {
067        super.service(smanager);
068        _contentSearcherFactory = (ContentSearcherFactory) smanager.lookup(ContentSearcherFactory.ROLE);
069        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
070    }
071    
072    @Override
073    public Map doAct(Request request, SearchUIModel searchModel, Map<String, Object> contextualParameters) throws Exception
074    {
075        request.setAttribute(JSonReader.OBJECT_TO_READ, _searchModelToJSON(searchModel, contextualParameters));
076        return EMPTY_MAP;
077    }
078    
079    /* we don't use searchModel.toJSON because we want to create a configuration with a format for external tool */
080    private Map<String, Object> _searchModelToJSON(SearchUIModel searchModel, Map<String, Object> contextualParameters)
081    {
082        Map<String, Object> json = new LinkedHashMap<>();
083        
084        json.put("name", searchModel.getId());
085        json.put("description", searchModel.getDescription());
086        
087        json.put("parameters", _getParametersAsJson(searchModel, contextualParameters));
088        json.put("output", _getOutputAsJson(searchModel, contextualParameters));
089        
090        return json;
091    }
092
093    private Map<String, Object> _getParametersAsJson(SearchUIModel searchModel, Map<String, Object> contextualParameters)
094    {
095        Map<String, Object> properties = new LinkedHashMap<>();
096        
097        // Serialize each items and then compute all the enum with one single query using facets
098        Map<String, Map<String, String>> facetsToCompute = new HashMap<>();
099        for (SearchModelCriterionViewItem viewItem : getCriteria(searchModel, contextualParameters))
100        {
101            SearchModelCriterionDefinition criterion = (SearchModelCriterionDefinition) viewItem.getDefinition();
102            if (!viewItem.isHidden()) // hidden item are not for user
103            {
104                properties.put(criterion.getName(), _viewItem2Json(viewItem));
105                
106                // detect if the criterion target the title of a content attribute
107                // if so remember it to compute the enum later
108                if (criterion instanceof ReferencingSearchModelCriterionDefinition referencingCriterion)
109                {
110                    int lastIndexOf = Strings.CS.lastIndexOf(referencingCriterion.getReferencePath(), ModelItem.ITEM_PATH_SEPARATOR);
111                    if (lastIndexOf != -1)
112                    {
113                        Map<String, String> criterionToDataMapping = facetsToCompute.computeIfAbsent(referencingCriterion.getReferencePath().substring(0, lastIndexOf) , __ -> new HashMap<>());
114                        criterionToDataMapping.put(criterion.getName(), referencingCriterion.getReferencePath().substring(lastIndexOf + 1));
115                    }
116                }
117            }
118        }
119        
120        // Edit the JSON to include enumeration based on facets
121        if (!facetsToCompute.isEmpty())
122        {
123            _includeFacetsInResults(searchModel, properties, facetsToCompute, contextualParameters);
124        }
125        
126        return Map.of(
127            "type", "object",
128            "additionalProperties", false,
129            "properties", properties
130        );
131    }
132    
133    private void _includeFacetsInResults(SearchUIModel searchModel, Map<String, Object> json, Map<String, Map<String, String>> facetsToCompute, Map<String, Object> contextualParameters)
134    {
135        try
136        {
137            SearchResults<Content> searchResults = _contentSearcherFactory.create(searchModel.getContentTypes(Map.of()))
138                    .withFacets(facetsToCompute.keySet())
139                    .withLimits(0, 1)
140                    .searchWithFacets(new MatchAllQuery());
141            
142            Map<String, Map<String, Integer>> facetResults = searchResults.getFacetResults();
143            for (Entry<String, Map<String, Integer>> facetResult : facetResults.entrySet())
144            {
145                Map<String, String> criterionToDataMapping = facetsToCompute.get(facetResult.getKey());
146                for (String criterionName: criterionToDataMapping.keySet())
147                {
148                    @SuppressWarnings("unchecked")
149                    Map<String, Object> criteriaJson = (Map<String, Object>) json.get(criterionName);
150                    List<Object> values = new ArrayList<>();
151                    for (String contentId : facetResult.getValue().keySet())
152                    {
153                        try
154                        {
155                            Content content = _resolver.resolveById(contentId);
156                            values.add(_elementToJson(content, (ElementDefinition) content.getDefinition(criterionToDataMapping.get(criterionName)), DataContext.newInstance(), contextualParameters));
157                        }
158                        catch (UnknownAmetysObjectException e)
159                        {
160                            // ignore
161                        }
162                    }
163                    criteriaJson.put("enum", values);
164                }
165            }
166        }
167        catch (Exception e)
168        {
169            getLogger().error("An error occured while trying to determine enum values");
170        }
171    }
172
173    private Map<String, Object> _viewItem2Json(ModelViewItem viewItem)
174    {
175        Map<String, Object> result = new LinkedHashMap<>();
176        
177        switch (viewItem.getDefinition().getType().getId())
178        {
179            case org.ametys.runtime.model.type.ModelItemTypeConstants.BOOLEAN_TYPE_ID:
180                result.put("type", "boolean");
181                break;
182            case org.ametys.runtime.model.type.ModelItemTypeConstants.LONG_TYPE_ID:
183                result.put("type", "integer");
184                break;
185            case org.ametys.runtime.model.type.ModelItemTypeConstants.DOUBLE_TYPE_ID:
186                result.put("type", "number");
187                break;
188            case org.ametys.runtime.model.type.ModelItemTypeConstants.DATE_TYPE_ID:
189                result.put("type", "string");
190                result.put("format", "date");
191                break;
192            case org.ametys.runtime.model.type.ModelItemTypeConstants.DATETIME_TYPE_ID:
193                result.put("type", "string");
194                result.put("format", "date-time");
195                break;
196            case org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID:
197            case ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
198            case ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID:
199            case ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
200            case ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID:
201                result.put("type", "string");
202                break;
203            default:
204                throw new UnsupportedOperationException("Unsupported type for criterion '" + viewItem.getName() + "'");
205        }
206        
207        if (viewItem.getDescription() != null)
208        {
209            result.put("description", viewItem.getDescription());
210        }
211        
212        return result;
213    }
214    
215    private Map<String, Object> _getOutputAsJson(SearchUIModel searchModel, Map<String, Object> contextualParameters)
216    {
217        Map<String, Object> properties = new LinkedHashMap<>();
218        
219        ViewItemContainer resultItems = searchModel.getResultItems(contextualParameters);
220        for (ViewItem viewItem : resultItems.getViewItems())
221        {
222            if (viewItem instanceof ViewElement element
223                    && (!(viewItem instanceof ViewItemAccessor accessor) || accessor.getViewItems().isEmpty()))
224            {
225                properties.put(element.getName(), _viewItem2Json(element));
226            }
227            // Support single nested column by "flattening" them to allow support for repeater entries values and linked content attributes
228            else if (viewItem instanceof ViewItemAccessor accessor && _canBeFlattened(accessor, resultItems))
229            {
230                SearchUIColumn item = (SearchUIColumn) accessor.getViewItems().getFirst();
231                properties.put(item.getName(), _viewItem2Json(item));
232            }
233            else
234            {
235                throw new UnsupportedOperationException("The requested search model is not supported");
236            }
237        }
238        
239        properties.put("url", Map.of("type", "string"));
240        properties.put("snippets", Map.of("type", "object"));
241        
242        Map<String, Object> result = new LinkedHashMap<>();
243        result.put("type", "array");
244        
245        Map<String, Object> items = new LinkedHashMap<>();
246        items.put("type", "object");
247        items.put("properties", properties);
248        result.put("items", items);
249        return result;
250    }
251}