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