001/* 002 * Copyright 2013 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.plugins.queriesdirectory; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import javax.jcr.RepositoryException; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.jackrabbit.util.ISO9075; 033 034import org.ametys.cms.repository.Content; 035import org.ametys.cms.search.SortOrder; 036import org.ametys.cms.search.content.ContentSearcherFactory; 037import org.ametys.cms.search.content.ContentSearcherFactory.ContentSearchSort; 038import org.ametys.cms.search.content.ContentSearcherFactory.SearchModelContentSearcher; 039import org.ametys.cms.search.ui.model.SearchUIModel; 040import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 041import org.ametys.core.util.JSONUtils; 042import org.ametys.plugins.repository.AmetysObject; 043import org.ametys.plugins.repository.AmetysObjectIterable; 044import org.ametys.plugins.repository.AmetysObjectResolver; 045import org.ametys.plugins.repository.AmetysRepositoryException; 046import org.ametys.plugins.repository.query.expression.StringExpression; 047import org.ametys.plugins.repository.query.expression.Expression; 048import org.ametys.plugins.repository.query.expression.Expression.Operator; 049import org.ametys.plugins.repository.query.expression.OrExpression; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051 052/** 053 * Helper for manipulating {@link Query} 054 * 055 */ 056public class QueryHelper extends AbstractLogEnabled implements Serviceable, Component 057{ 058 /** Avalon Role */ 059 public static final String ROLE = QueryHelper.class.getName(); 060 061 /** The Ametys object resolver */ 062 protected AmetysObjectResolver _resolver; 063 064 /** JSON Utils */ 065 protected JSONUtils _jsonUtils; 066 067 /** SearchUI Model Extension Point */ 068 protected SearchUIModelExtensionPoint _searchUiEP; 069 070 /** Content Searcher Factory */ 071 protected ContentSearcherFactory _contentSearcherFactory; 072 073 /** The service manager */ 074 protected ServiceManager _manager; 075 076 public void service(ServiceManager manager) throws ServiceException 077 { 078 _manager = manager; 079 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 080 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 081 _searchUiEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE); 082 _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE); 083 } 084 085 /** 086 * Creates the XPath query to get all query containers 087 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 088 * @return The XPath query 089 */ 090 static String getXPathForQueryContainers(QueryContainer queryContainer) 091 { 092 return _getXPathQuery(queryContainer, true, ObjectToReturn.QUERY_CONTAINER, List.of()); 093 } 094 095 /** 096 * Creates the XPath query to get all queries for administrator 097 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 098 * @param onlyDirectChildren <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 099 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 100 * @return The XPath query 101 */ 102 static String getXPathForQueriesForAdministrator(QueryContainer queryContainer, boolean onlyDirectChildren, List<String> acceptedTypes) 103 { 104 return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, acceptedTypes); 105 } 106 107 /** 108 * Creates the XPath query to get all queries in WRITE access 109 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 110 * @param onlyDirectChildren <code>true</code> in order to have only direct child queries from parent path, <code>false</code> otherwise to have all queries at any level underneath the parent path 111 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 112 * @return The XPath query 113 */ 114 static String getXPathForQueries(QueryContainer queryContainer, boolean onlyDirectChildren, List<String> acceptedTypes) 115 { 116 return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, acceptedTypes); 117 } 118 119 private static StringBuilder _getParentPath(QueryContainer queryContainer) 120 { 121 try 122 { 123 StringBuilder parentPath = new StringBuilder("/jcr:root") 124 .append(ISO9075.encodePath(queryContainer.getNode().getPath())); 125 return parentPath; 126 } 127 catch (RepositoryException e) 128 { 129 throw new AmetysRepositoryException(e); 130 } 131 } 132 133 private static String _getXPathQuery(QueryContainer queryContainer, boolean onlyDirectChildren, ObjectToReturn objectToReturn, List<String> acceptedTypes) 134 { 135 StringBuilder parentPath = _getParentPath(queryContainer); 136 final String slashOrDoubleSlash = onlyDirectChildren ? "/" : "//"; 137 StringBuilder query = parentPath 138 .append(slashOrDoubleSlash) 139 .append("element(*, ") 140 .append(objectToReturn.toNodetype()) 141 .append(")"); 142 if (acceptedTypes != null && !acceptedTypes.isEmpty()) 143 { 144 List<Expression> exprs = new ArrayList<>(); 145 for (String type : acceptedTypes) 146 { 147 exprs.add(new StringExpression(Query.TYPE, Operator.EQ, type)); 148 } 149 Expression typeExpression = new OrExpression(exprs.toArray(new Expression[exprs.size()])); 150 query.append("[").append(typeExpression.build()).append("]"); 151 } 152 153 return query.toString(); 154 } 155 156 private static enum ObjectToReturn 157 { 158 QUERY, 159 QUERY_CONTAINER; 160 161 private String _nodetype; 162 163 static 164 { 165 QUERY._nodetype = QueryFactory.QUERY_NODETYPE; 166 QUERY_CONTAINER._nodetype = QueryContainerFactory.QUERY_CONTAINER_NODETYPE; 167 } 168 169 String toNodetype() 170 { 171 return _nodetype; 172 } 173 } 174 175 176 /** 177 * Execute a query 178 * @param queryId id of the query to execute 179 * @return the results of the query 180 * @throws Exception something went wrong 181 */ 182 public AmetysObjectIterable<Content> executeQuery(String queryId) throws Exception 183 { 184 AmetysObject ametysObject = _resolver.resolveById(queryId); 185 if (ametysObject instanceof Query) 186 { 187 return executeQuery((Query) ametysObject); 188 } 189 else 190 { 191 return null; 192 } 193 } 194 195 /** 196 * Execute a query 197 * @param query the query to execute 198 * @return the results of the query, can be null 199 * @throws Exception if failed to execute query 200 */ 201 public AmetysObjectIterable<Content> executeQuery(Query query) throws Exception 202 { 203 Map<String, Object> exportParams = getExportParams(query); 204 205 int limit = getLimitForQuery(exportParams); 206 List<ContentSearchSort> sort = getSortForQuery(exportParams); 207 208 String model = getModelForQuery(exportParams); 209 if ("solr".equals(query.getType())) 210 { 211 String queryStr = getQueryStringForQuery(exportParams); 212 Set<String> contentTypeIds = getContentTypesForQuery(exportParams).stream().collect(Collectors.toSet()); 213 AmetysObjectIterable<Content> results = _contentSearcherFactory.create(contentTypeIds) 214 .withSort(sort) 215 .withLimits(0, limit) 216 .search(queryStr); 217 218 return results; 219 } 220 else if (Query.Type.SIMPLE.toString().equals(query.getType()) || Query.Type.ADVANCED.toString().equals(query.getType())) 221 { 222 Map<String, Object> values = getValuesForQuery(exportParams); 223 224 Map<String, Object> contextualParameters = getContextualParametersForQuery(exportParams); 225 String searchMode = getSearchModeForQuery(exportParams); 226 SearchUIModel uiModel = _searchUiEP.getExtension(model); 227 228 SearchModelContentSearcher searcher = _contentSearcherFactory.create(uiModel); 229 230 return searcher 231 .withLimits(0, limit) 232 .withSort(sort) 233 .withSearchMode(searchMode) 234 .search(values, contextualParameters); 235 } 236 else 237 { 238 getLogger().warn("This method should only handle solr, advanced or simple queries. Query id '{}' is type '{}'", query.getId(), query.getType()); 239 return null; 240 } 241 } 242 243 /** 244 * Get the limit of results stored in a query, or -1 if none is found 245 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 246 * @return the maximum number of results that this query should return, -1 if no limit 247 */ 248 public int getLimitForQuery(Map<String, Object> exportParams) 249 { 250 return Optional.of("limit") 251 .map(exportParams::get) 252 .map(Integer.class::cast) 253 .filter(l -> l >= 0) 254 .orElse(Integer.MAX_VALUE); 255 } 256 257 /** 258 * Get the model of the query based on exportParams 259 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 260 * @return the model of the query 261 */ 262 public String getModelForQuery(Map<String, Object> exportParams) 263 { 264 if (exportParams.containsKey("model")) 265 { 266 return (String) exportParams.get("model"); 267 } 268 return null; 269 } 270 271 /** 272 * Get the values of the query based on exportParams 273 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 274 * @return the values of the query 275 */ 276 public Map<String, Object> getValuesForQuery(Map<String, Object> exportParams) 277 { 278 if (exportParams.containsKey("values")) 279 { 280 @SuppressWarnings("unchecked") 281 Map<String, Object> values = (Map<String, Object>) exportParams.get("values"); 282 return values; 283 } 284 return null; 285 } 286 287 /** 288 * Get the contextual parameters of the query based on exportParams 289 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 290 * @return the contextual parameters of the query 291 */ 292 public Map<String, Object> getContextualParametersForQuery(Map<String, Object> exportParams) 293 { 294 if (exportParams.containsKey("contextualParameters")) 295 { 296 @SuppressWarnings("unchecked") 297 Map<String, Object> contextualParameters = (Map<String, Object>) exportParams.get("contextualParameters"); 298 return contextualParameters; 299 } 300 return null; 301 } 302 303 /** 304 * Get the sort of the query based on exportParams 305 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 306 * @return the sort of the query 307 */ 308 public List<ContentSearchSort> getSortForQuery(Map<String, Object> exportParams) 309 { 310 if (exportParams.containsKey("sort")) 311 { 312 String sortString = (String) exportParams.get("sort"); 313 List<Object> sortlist = _jsonUtils.convertJsonToList(sortString); 314 return sortlist.stream() 315 .map(Map.class::cast) 316 .map(map -> new ContentSearchSort((String) map.get("property"), SortOrder.valueOf((String) map.get("direction")))) 317 .collect(Collectors.toList()); 318 } 319 return null; 320 } 321 322 /** 323 * Get the Content Types of the query based on exportParams 324 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 325 * @return the Content Types of the query 326 */ 327 public List<String> getContentTypesForQuery(Map<String, Object> exportParams) 328 { 329 Map<String, Object> values = getValuesForQuery(exportParams); 330 if (values.containsKey("contentTypes")) 331 { 332 @SuppressWarnings("unchecked") 333 List<String> contentTypes = (List<String>) values.get("contentTypes"); 334 return contentTypes; 335 } 336 return null; 337 } 338 339 /** 340 * Get the solr query string of the query based on exportParams 341 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 342 * @return the solr query string of the query 343 */ 344 public String getQueryStringForQuery(Map<String, Object> exportParams) 345 { 346 Map<String, Object> values = getValuesForQuery(exportParams); 347 if (values.containsKey("query")) 348 { 349 return (String) values.get("query"); 350 } 351 return null; 352 } 353 354 /** 355 * Get the search model of the query based on exportParams 356 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 357 * @return the search model of the query 358 */ 359 public String getSearchModeForQuery(Map<String, Object> exportParams) 360 { 361 if (exportParams.containsKey("searchMode")) 362 { 363 return (String) exportParams.get("searchMode"); 364 } 365 return "simple"; 366 } 367 368 /** 369 * Get the export params of the query 370 * @param query the query 371 * @return the export params of the query 372 */ 373 public Map<String, Object> getExportParams(Query query) 374 { 375 String queryContent = query.getContent(); 376 Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(queryContent); 377 if (jsonMap.containsKey("exportParams")) 378 { 379 Object exportParams = jsonMap.get("exportParams"); 380 if (exportParams instanceof Map<?, ?>) 381 { 382 @SuppressWarnings("unchecked") 383 Map<String, Object> exportParamsObject = (Map<String, Object>) exportParams; 384 return exportParamsObject; 385 } 386 } 387 return new HashMap<>(); 388 } 389}