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.group.GroupIdentity; 040import org.ametys.core.user.UserIdentity; 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.runtime.plugin.component.AbstractLogEnabled; 047 048/** 049 * Helper for manipulating {@link Query} 050 * 051 */ 052public class QueryHelper extends AbstractLogEnabled implements Serviceable, Component 053{ 054 /** Avalon Role */ 055 public static final String ROLE = QueryHelper.class.getName(); 056 057 /** The Ametys object resolver */ 058 protected AmetysObjectResolver _resolver; 059 060 /** JSON Utils */ 061 protected JSONUtils _jsonUtils; 062 063 /** SearchUI Model Extension Point */ 064 protected SearchUIModelExtensionPoint _searchUiEP; 065 066 /** Content Searcher Factory */ 067 protected ContentSearcherFactory _contentSearcherFactory; 068 069 /** The service manager */ 070 protected ServiceManager _manager; 071 072 public void service(ServiceManager manager) throws ServiceException 073 { 074 _manager = manager; 075 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 076 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 077 _searchUiEP = (SearchUIModelExtensionPoint) manager.lookup(SearchUIModelExtensionPoint.ROLE); 078 _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE); 079 } 080 081 /** 082 * Creates the XPath query to get all query containers 083 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 084 * @return The XPath query 085 */ 086 static String getXPathForQueryContainers(QueryContainer queryContainer) 087 { 088 return _getXPathQuery(queryContainer, true, ObjectToReturn.QUERY_CONTAINER, Optional.empty(), Optional.empty(), null); 089 } 090 091 /** 092 * Creates the XPath query to get all queries for administrator 093 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 094 * @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 095 * @param type The query type 096 * @return The XPath query 097 */ 098 static String getXPathForQueriesForAdministrator(QueryContainer queryContainer, boolean onlyDirectChildren, Optional<String> type) 099 { 100 return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.empty(), type, null); 101 } 102 103 /** 104 * Creates the XPath query to get all queries in READ access 105 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 106 * @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 107 * @param visibility The user and its groups for checking visibility 108 * @param type The query type 109 * @return The XPath query 110 */ 111 static String getXPathForQueriesInReadAccess(QueryContainer queryContainer, boolean onlyDirectChildren, Visibility visibility, Optional<String> type) 112 { 113 return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.of(visibility), type, true); 114 } 115 116 /** 117 * Creates the XPath query to get all queries in WRITE access 118 * @param queryContainer The {@link QueryContainer}, defining the context from which getting children 119 * @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 120 * @param visibility The user and its groups for checking visibility 121 * @param type The query type 122 * @return The XPath query 123 */ 124 static String getXPathForQueriesInWriteAccess(QueryContainer queryContainer, boolean onlyDirectChildren, Visibility visibility, Optional<String> type) 125 { 126 return _getXPathQuery(queryContainer, onlyDirectChildren, ObjectToReturn.QUERY, Optional.of(visibility), type, false); 127 } 128 129 private static StringBuilder _getParentPath(QueryContainer queryContainer) 130 { 131 try 132 { 133 StringBuilder parentPath = new StringBuilder("/jcr:root") 134 .append(ISO9075.encodePath(queryContainer.getNode().getPath())); 135 return parentPath; 136 } 137 catch (RepositoryException e) 138 { 139 throw new AmetysRepositoryException(e); 140 } 141 } 142 143 private static String _getXPathQuery(QueryContainer queryContainer, boolean onlyDirectChildren, ObjectToReturn objectToReturn, Optional<Visibility> visibility, Optional<String> type, Boolean readAccess) 144 { 145 StringBuilder parentPath = _getParentPath(queryContainer); 146 147 final String slashOrDoubleSlash = onlyDirectChildren ? "/" : "//"; 148 StringBuilder query = parentPath 149 .append(slashOrDoubleSlash) 150 .append("element(*, ") 151 .append(objectToReturn.toNodetype()) 152 .append(")"); 153 154 if (visibility.isPresent() || type.isPresent()) 155 { 156 query.append('['); 157 } 158 159 type.ifPresent(t -> query.append("@").append(Query.TYPE).append("='").append(t).append("'")); 160 161 if (visibility.isPresent()) 162 { 163 type.ifPresent(t -> query.append(" and (")); 164 165 _appendPublicPredicate(query); 166 167 UserIdentity user = visibility.get().getUser(); 168 String login = user.getLogin(); 169 String populationId = user.getPopulationId(); 170 _appendPrivatePredicate(query, login, populationId); 171 172 Set<GroupIdentity> groups = visibility.get().getGroups(); 173 _appendSharedPredicate(query, login, populationId, groups, readAccess); 174 175 type.ifPresent(t -> query.append(')')); 176 } 177 178 179 if (visibility.isPresent() || type.isPresent()) 180 { 181 query.append(']'); 182 } 183 184 return query.toString(); 185 } 186 187 private static void _appendPublicPredicate(StringBuilder query) 188 { 189 query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.PUBLIC.name()) 190 .append("') or "); 191 } 192 193 private static void _appendPrivatePredicate(StringBuilder query, String login, String populationId) 194 { 195 query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.PRIVATE.name()) 196 .append("' and "); 197 _appendUserCondition(query, login, populationId); 198 query.append(") or "); 199 } 200 201 private static void _appendSharedPredicate(StringBuilder query, String login, String populationId, Set<GroupIdentity> groups, boolean readAccess) 202 { 203 query.append("(@").append(Query.VISIBILITY).append("='").append(Query.Visibility.SHARED.name()) 204 .append("' and ("); 205 query.append("("); 206 _appendUserCondition(query, login, populationId); 207 query.append(") or "); 208 209 if (readAccess) 210 { 211 _appendUserAccessCondition(query, login, populationId, Query.PROFILE_READ_ACCESS); 212 query.append(" or "); 213 } 214 _appendUserAccessCondition(query, login, populationId, Query.PROFILE_WRITE_ACCESS); 215 216 _appendGroupsCondition(query, groups, readAccess); 217 query.append("))"); 218 } 219 220 private static void _appendUserCondition(StringBuilder query, String login, String populationId) 221 { 222 query.append('@').append(Query.AUTHOR).append("/ametys:login='").append(login) 223 .append("' and @").append(Query.AUTHOR).append("/ametys:population='").append(populationId) 224 .append('\''); 225 } 226 227 private static void _appendGroupsCondition(StringBuilder query, Set<GroupIdentity> groups, boolean readAccess) 228 { 229 for (GroupIdentity group : groups) 230 { 231 String groupId = group.getId(); 232 String groupDirectory = group.getDirectoryId(); 233 234 if (readAccess) 235 { 236 query.append(" or "); 237 _appendGroupAccessCondition(query, groupId, groupDirectory, Query.PROFILE_READ_ACCESS); 238 } 239 query.append(" or "); 240 _appendGroupAccessCondition(query, groupId, groupDirectory, Query.PROFILE_WRITE_ACCESS); 241 } 242 } 243 244 private static void _appendUserAccessCondition(StringBuilder query, String login, String populationId, String accessNodeName) 245 { 246 query.append("(").append(accessNodeName).append("/").append(Query.USERS).append("/ametys:login='").append(login) 247 .append("' and ").append(accessNodeName).append("/").append(Query.USERS).append("/ametys:population='").append(populationId) 248 .append("')"); 249 } 250 251 private static void _appendGroupAccessCondition(StringBuilder query, String groupId, String groupDirectory, String accessNodeName) 252 { 253 query.append("(").append(accessNodeName).append("/").append(Query.GROUPS).append("/ametys:groupId='").append(groupId) 254 .append("' and ").append(accessNodeName).append("/").append(Query.GROUPS).append("/ametys:groupDirectory='").append(groupDirectory) 255 .append("')"); 256 } 257 258 /** 259 * The user and its groups for checking visibility 260 */ 261 public static final class Visibility 262 { 263 private UserIdentity _user; 264 private Set<GroupIdentity> _groups; 265 266 private Visibility(UserIdentity user, Set<GroupIdentity> groups) 267 { 268 _user = user; 269 _groups = groups; 270 } 271 272 /** 273 * Creates a new {@link Visibility} 274 * @param user The user 275 * @param groups The user groups 276 * @return the {@link Visibility} wrapper 277 */ 278 public static Visibility of(UserIdentity user, Set<GroupIdentity> groups) 279 { 280 return new Visibility(user, groups); 281 } 282 283 UserIdentity getUser() 284 { 285 return _user; 286 } 287 288 Set<GroupIdentity> getGroups() 289 { 290 return _groups; 291 } 292 } 293 294 private static enum ObjectToReturn 295 { 296 QUERY, 297 QUERY_CONTAINER; 298 299 private String _nodetype; 300 301 static 302 { 303 QUERY._nodetype = QueryFactory.QUERY_NODETYPE; 304 QUERY_CONTAINER._nodetype = QueryContainerFactory.QUERY_CONTAINER_NODETYPE; 305 } 306 307 String toNodetype() 308 { 309 return _nodetype; 310 } 311 } 312 313 314 /** 315 * Execute a query 316 * @param queryId id of the query to execute 317 * @return the results of the query 318 * @throws Exception something went wrong 319 */ 320 public AmetysObjectIterable<Content> executeQuery(String queryId) throws Exception 321 { 322 AmetysObject ametysObject = _resolver.resolveById(queryId); 323 if (ametysObject instanceof Query) 324 { 325 return executeQuery((Query) ametysObject); 326 } 327 else 328 { 329 return null; 330 } 331 } 332 333 /** 334 * Execute a query 335 * @param query the query to execute 336 * @return the results of the query, can be null 337 * @throws Exception something went wrong 338 */ 339 public AmetysObjectIterable<Content> executeQuery(Query query) throws Exception 340 { 341 Map<String, Object> exportParams = getExportParams(query); 342 343 int limit = getLimitForQuery(exportParams); 344 List<Sort> sort = getSortForQuery(exportParams); 345 346 String model = getModelForQuery(exportParams); 347 if ("solr".equals(query.getType())) 348 { 349 String queryStr = getQueryStringForQuery(exportParams); 350 Set<String> contentTypeIds = getContentTypesForQuery(exportParams).stream().collect(Collectors.toSet()); 351 AmetysObjectIterable<Content> results = _contentSearcherFactory.create(contentTypeIds) 352 .withSort(sort) 353 .withLimits(0, limit) 354 .search(queryStr); 355 356 return results; 357 } 358 else if (Query.Type.SIMPLE.toString().equals(query.getType()) || Query.Type.ADVANCED.toString().equals(query.getType())) 359 { 360 Map<String, Object> values = getValuesForQuery(exportParams); 361 Map<String, Object> contextualParameters = getContextualParametersForQuery(exportParams); 362 String searchMode = getSearchModeForQuery(exportParams); 363 SearchUIModel uiModel = _searchUiEP.getExtension(model); 364 365 SearchModelContentSearcher searcher = _contentSearcherFactory.create(uiModel); 366 367 return searcher 368 .withLimits(0, limit) 369 .withSort(sort) 370 .withSearchMode(searchMode) 371 .search(values, contextualParameters); 372 } 373 else 374 { 375 getLogger().warn("This method should only handle solr, advanced or simole queries. query id '" + query.getId() + "' is type '" + query.getType() + "'"); 376 return null; 377 } 378 } 379 380 /** 381 * Get the limit of results stored in a query, or -1 if none is found 382 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 383 * @return the maximum number of results that this query should return, -1 if no limit 384 */ 385 public int getLimitForQuery(Map<String, Object> exportParams) 386 { 387 return Optional.of("limit") 388 .map(exportParams::get) 389 .map(Integer.class::cast) 390 .filter(l -> l >= 0) 391 .orElse(Integer.MAX_VALUE); 392 } 393 394 /** 395 * Get the model of the query based on exportParams 396 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 397 * @return the model of the query 398 */ 399 public String getModelForQuery(Map<String, Object> exportParams) 400 { 401 if (exportParams.containsKey("model")) 402 { 403 return (String) exportParams.get("model"); 404 } 405 return null; 406 } 407 408 /** 409 * Get the values of the query based on exportParams 410 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 411 * @return the values of the query 412 */ 413 public Map<String, Object> getValuesForQuery(Map<String, Object> exportParams) 414 { 415 if (exportParams.containsKey("values")) 416 { 417 @SuppressWarnings("unchecked") 418 Map<String, Object> values = (Map<String, Object>) exportParams.get("values"); 419 return values; 420 } 421 return null; 422 } 423 424 /** 425 * Get the contextual parameters of the query based on exportParams 426 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 427 * @return the contextual parameters of the query 428 */ 429 public Map<String, Object> getContextualParametersForQuery(Map<String, Object> exportParams) 430 { 431 if (exportParams.containsKey("contextualParameters")) 432 { 433 @SuppressWarnings("unchecked") 434 Map<String, Object> contextualParameters = (Map<String, Object>) exportParams.get("contextualParameters"); 435 return contextualParameters; 436 } 437 return null; 438 } 439 440 /** 441 * Get the sort of the query based on exportParams 442 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 443 * @return the sort of the query 444 */ 445 public List<Sort> getSortForQuery(Map<String, Object> exportParams) 446 { 447 if (exportParams.containsKey("sort")) 448 { 449 String sortString = (String) exportParams.get("sort"); 450 List<Object> sortlist = _jsonUtils.convertJsonToList(sortString); 451 return sortlist.stream() 452 .map(Map.class::cast) 453 .map(map -> new Sort((String) map.get("property"), Sort.Order.valueOf((String) map.get("direction")))) 454 .collect(Collectors.toList()); 455 } 456 return null; 457 } 458 459 /** 460 * Get the Content Types of the query based on exportParams 461 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 462 * @return the Content Types of the query 463 */ 464 public List<String> getContentTypesForQuery(Map<String, Object> exportParams) 465 { 466 Map<String, Object> values = getValuesForQuery(exportParams); 467 if (values.containsKey("contentTypes")) 468 { 469 @SuppressWarnings("unchecked") 470 List<String> contentTypes = (List<String>) values.get("contentTypes"); 471 return contentTypes; 472 } 473 return null; 474 } 475 476 /** 477 * Get the solr query string of the query based on exportParams 478 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 479 * @return the solr query string of the query 480 */ 481 public String getQueryStringForQuery(Map<String, Object> exportParams) 482 { 483 Map<String, Object> values = getValuesForQuery(exportParams); 484 if (values.containsKey("query")) 485 { 486 return (String) values.get("query"); 487 } 488 return null; 489 } 490 491 /** 492 * Get the search model of the query based on exportParams 493 * @param exportParams export params of the query, available via {@link QueryHelper#getExportParams(Query)} 494 * @return the search model of the query 495 */ 496 public String getSearchModeForQuery(Map<String, Object> exportParams) 497 { 498 if (exportParams.containsKey("searchMode")) 499 { 500 return (String) exportParams.get("searchMode"); 501 } 502 return "simple"; 503 } 504 505 /** 506 * Get the export params of the query 507 * @param query the query 508 * @return the export params of the query 509 */ 510 public Map<String, Object> getExportParams(Query query) 511 { 512 String queryContent = query.getContent(); 513 Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(queryContent); 514 if (jsonMap.containsKey("exportParams")) 515 { 516 Object exportParams = jsonMap.get("exportParams"); 517 if (exportParams instanceof Map<?, ?>) 518 { 519 @SuppressWarnings("unchecked") 520 Map<String, Object> exportParamsObject = (Map<String, Object>) exportParams; 521 return exportParamsObject; 522 } 523 } 524 return new HashMap<>(); 525 } 526}