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