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