001/* 002 * Copyright 2017 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; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Optional; 026import java.util.Set; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.logger.AbstractLogEnabled; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034 035import org.ametys.cms.search.content.ContentSearcherFactory.ContentSearchSort; 036import org.ametys.cms.search.model.SearchModel; 037import org.ametys.cms.search.model.SearchModelHelper; 038import org.ametys.cms.search.query.AndQuery; 039import org.ametys.cms.search.query.ContentTypeOrMixinTypeQuery; 040import org.ametys.cms.search.query.ContentTypeQuery; 041import org.ametys.cms.search.query.MixinTypeQuery; 042import org.ametys.cms.search.query.Query; 043import org.ametys.cms.search.query.Query.Operator; 044import org.ametys.cms.search.query.QuerySyntaxException; 045import org.ametys.cms.search.ui.model.SearchUIColumn; 046import org.ametys.core.util.JSONUtils; 047import org.ametys.runtime.model.ViewHelper; 048import org.ametys.runtime.model.ViewItem; 049import org.ametys.runtime.model.ViewItemAccessor; 050import org.ametys.runtime.model.ViewItemContainer; 051 052/** 053 * Helper to get query infos from JSON object 054 */ 055public class GetQueryFromJSONHelper extends AbstractLogEnabled implements Component, Serviceable 056{ 057 /** The Avalon role name */ 058 public static final String ROLE = GetQueryFromJSONHelper.class.getName(); 059 060 private QueryBuilder _queryBuilder; 061 private JSONUtils _jsonUtils; 062 private SearchModelHelper _searchModelHelper; 063 064 public void service(ServiceManager serviceManager) throws ServiceException 065 { 066 _queryBuilder = (QueryBuilder) serviceManager.lookup(QueryBuilder.ROLE); 067 _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE); 068 _searchModelHelper = (SearchModelHelper) serviceManager.lookup(SearchModelHelper.ROLE); 069 } 070 071 /** 072 * Retrieves the search UI model 073 * @param parameters The JS parameters 074 * @return the search UI model 075 */ 076 public SearchModel getSearchModel(Map<String, Object> parameters) 077 { 078 return getSearchModel(parameters, "model"); 079 } 080 081 /** 082 * Retrieves the search UI model 083 * @param parameters The JS parameters 084 * @param modelKeyName key name of the model parameter 085 * @return the search UI model 086 */ 087 @SuppressWarnings("unchecked") 088 public SearchModel getSearchModel(Map<String, Object> parameters, String modelKeyName) 089 { 090 String modelId = (String) parameters.get(modelKeyName); 091 Map<String, Object> contextualParameters = Optional.ofNullable((Map<String, Object>) parameters.get("contextualParameters")).orElse(new HashMap<>()); 092 return _searchModelHelper.getRestrictedSearchModel(modelId, contextualParameters); 093 } 094 095 /** 096 * Retrieves a Query object from the SearchUIModel and the JSON parameters 097 * @param model Model 098 * @param parameters JSON parameters 099 * @param contentTypes List of content types to fill 100 * @return the created Query object 101 * @throws QuerySyntaxException If an error occurs during the query parsing 102 */ 103 @SuppressWarnings("unchecked") 104 public Query getQueryFromModel(SearchModel model, Map<String, Object> parameters, List<String> contentTypes) throws QuerySyntaxException 105 { 106 Map<String, Object> contextualParameters = Optional.ofNullable((Map<String, Object>) parameters.get("contextualParameters")).orElseGet(HashMap::new); 107 108 Map<String, Object> values = (Map<String, Object>) parameters.get("values"); 109 String searchMode = Objects.toString(parameters.get("searchMode"), "simple"); 110 111 Query query = _queryBuilder.build(model, searchMode , values, contextualParameters); 112 return _extractContentTypesFromQuery(query, contentTypes); 113 } 114 115 private Query _extractContentTypesFromQuery(Query query, List<String> contentTypes) 116 { 117 Query newQuery = query; 118 if (query instanceof AndQuery andQuery) 119 { 120 boolean foundContentTypes = false; 121 122 Set<Query> subQueries = new LinkedHashSet<>(andQuery.getQueries()); 123 Query matchingQuery = _getFirstMatchingQuery(subQueries); 124 if (matchingQuery != null) 125 { 126 List<String> extractedContentTypes = _getContentTypesFromQuery(matchingQuery); 127 if (!extractedContentTypes.isEmpty()) 128 { 129 contentTypes.addAll(extractedContentTypes); 130 foundContentTypes = true; 131 132 // Change the query by putting the same AndQuery but without the part about contentTypes which are in the contentTypes field 133 subQueries.remove(matchingQuery); 134 newQuery = new AndQuery(subQueries); 135 } 136 } 137 138 if (!foundContentTypes) 139 { 140 // No content types query has been found => check recursively in sub-sub-queries 141 Set<Query> newSubQueries = new LinkedHashSet<>(); 142 for (Query subQuery : subQueries) 143 { 144 Query newSubQuery = _extractContentTypesFromQuery(subQuery, contentTypes); 145 newSubQueries.add(newSubQuery); 146 } 147 148 newQuery = new AndQuery(newSubQueries); 149 } 150 } 151 152 return newQuery; 153 } 154 155 private Query _getFirstMatchingQuery(Set<Query> queries) 156 { 157 // Find the first `ContentTypeQuery` with EQ operator 158 Optional<? extends Query> contentTypeQuery = queries.stream() 159 .filter(ContentTypeQuery.class::isInstance) 160 .map(ContentTypeQuery.class::cast) 161 .filter(query -> Operator.EQ.equals(query.getOperator())) 162 .findFirst(); 163 if (contentTypeQuery.isPresent()) 164 { 165 return contentTypeQuery.get(); 166 } 167 168 // Find the first `ContentTypeOrMixinTypeQuery` with EQ operator 169 contentTypeQuery = queries.stream() 170 .filter(ContentTypeOrMixinTypeQuery.class::isInstance) 171 .map(ContentTypeOrMixinTypeQuery.class::cast) 172 .filter(query -> Operator.EQ.equals(query.getOperator())) 173 .findFirst(); 174 if (contentTypeQuery.isPresent()) 175 { 176 return contentTypeQuery.get(); 177 } 178 179 // Find the first `MixinTypeQuery` with EQ operator 180 contentTypeQuery = queries.stream() 181 .filter(MixinTypeQuery.class::isInstance) 182 .map(MixinTypeQuery.class::cast) 183 .filter(query -> Operator.EQ.equals(query.getOperator())) 184 .findFirst(); 185 return contentTypeQuery.orElse(null); 186 } 187 188 private List<String> _getContentTypesFromQuery(Query query) 189 { 190 List<String> contentTypes = new ArrayList<>(); 191 192 if (query instanceof ContentTypeQuery cTypeQuery) 193 { 194 contentTypes.addAll(cTypeQuery.getValue()); 195 } 196 else if (query instanceof ContentTypeOrMixinTypeQuery cTypeOrMixinQuery) 197 { 198 contentTypes.addAll(cTypeOrMixinQuery.getIds()); 199 } 200 else if (query instanceof MixinTypeQuery mixinQuery) 201 { 202 contentTypes.addAll(mixinQuery.getValue()); 203 } 204 205 return contentTypes; 206 } 207 208 /** 209 * Retrieves a list of column ids 210 * @param model search model containing result fields 211 * @param contextualParameters JSON object containing the contextual parameters 212 * @return a list of columns ids 213 */ 214 public List<String> getColumnsFromSearchModel(SearchModel model, Map<String, Object> contextualParameters) 215 { 216 ViewItemContainer resultItems = model.getResultItems(contextualParameters != null ? contextualParameters : Collections.EMPTY_MAP); 217 return _getColumnIds(resultItems); 218 } 219 220 private List<String> _getColumnIds(ViewItemAccessor viewItemAccessor) 221 { 222 List<String> columnIds = new ArrayList<>(); 223 224 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 225 { 226 if (viewItem instanceof SearchUIColumn searchUIColumn) 227 { 228 String columnId = ViewHelper.getModelViewItemPath(searchUIColumn); 229 columnIds.add(columnId); 230 } 231 else if (viewItem instanceof ViewItemAccessor itemAccessor) 232 { 233 columnIds.addAll(_getColumnIds(itemAccessor)); 234 } 235 } 236 237 return columnIds; 238 } 239 240 /** 241 * Retrieves the sort criteria from a sort string. 242 * @param parameters The parameters (containing sort and optionally group keys) 243 * @return the sort criteria as a List of {@link ContentSearchSort}. 244 */ 245 public List<ContentSearchSort> getSort(Map<String, Object> parameters) 246 { 247 String sortString = (String) parameters.get("sort"); 248 String groupString = (String) parameters.get("group"); 249 return getSort(sortString, groupString); 250 } 251 252 /** 253 * Retrieves the sort criteria from a sort string. 254 * @param sortString The sort criteria as a JSON-encoded string. 255 * @param groupString The group criteria as a JSON-encoded string (for server-side grouping feature). Can be null. 256 * @return the sort criteria as a List of {@link ContentSearchSort}. 257 */ 258 public List<ContentSearchSort> getSort(String sortString, String groupString) 259 { 260 List<ContentSearchSort> sort = new ArrayList<>(); 261 262 List<Object> sortList = new ArrayList<>(_jsonUtils.convertJsonToList(sortString)); 263 if (StringUtils.isNotEmpty(groupString)) 264 { 265 // Grouping will be treated server side as a sort. It just needs to be before all the sorters 266 sortList.add(0, _jsonUtils.convertJsonToMap(groupString)); 267 } 268 269 for (Object object : sortList) 270 { 271 if (object instanceof Map) 272 { 273 Map map = (Map) object; 274 String fieldId = (String) map.get("property"); 275 boolean ascending = "ASC".equals(map.get("direction")); 276 277 sort.add(new ContentSearchSort(fieldId, ascending ? SortOrder.ASC : SortOrder.DESC)); 278 } 279 } 280 281 return sort; 282 } 283} 284