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}