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