001/* 002 * Copyright 2020 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.mobileapp; 017 018import java.time.Instant; 019import java.time.LocalDate; 020import java.time.ZoneId; 021import java.time.ZonedDateTime; 022import java.util.Collections; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.stream.Collectors; 031 032import org.apache.avalon.framework.component.Component; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.commons.lang3.StringUtils; 037 038import org.ametys.cms.contenttype.ContentTypesHelper; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.search.Sort; 041import org.ametys.cms.search.Sort.Order; 042import org.ametys.cms.search.content.ContentSearcherFactory; 043import org.ametys.cms.search.content.ContentSearcherFactory.SearchModelContentSearcher; 044import org.ametys.cms.search.ui.model.SearchUIModel; 045import org.ametys.cms.search.ui.model.SearchUIModelExtensionPoint; 046import org.ametys.cms.transformation.xslt.ResolveURIComponent; 047import org.ametys.core.util.DateUtils; 048import org.ametys.plugins.queriesdirectory.Query; 049import org.ametys.plugins.queriesdirectory.QueryContainer; 050import org.ametys.plugins.repository.AmetysObject; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.runtime.config.Config; 054import org.ametys.runtime.model.ModelItem; 055import org.ametys.runtime.model.exception.UndefinedItemPathException; 056import org.ametys.runtime.model.type.ModelItemTypeConstants; 057import org.ametys.runtime.plugin.component.AbstractLogEnabled; 058import org.ametys.web.repository.content.WebContent; 059import org.ametys.web.repository.page.Page; 060import org.ametys.web.repository.site.Site; 061import org.ametys.web.repository.site.SiteManager; 062 063import com.google.common.collect.ImmutableList; 064 065/** 066 * Manager to list and execute queries 067 */ 068public class QueriesHelper extends AbstractLogEnabled implements Serviceable, Component 069{ 070 /** Avalon Role */ 071 public static final String ROLE = QueriesHelper.class.getName(); 072 073 /** global configuration about queries */ 074 public static final String QUERY_CONTAINER_CONF_ID = "plugin.mobileapp.query.container"; 075 076 /** site configuration about queries */ 077 public static final String QUERY_CONTAINER_SITE_CONF_ID = "mobileapp-query-container"; 078 079 /** global configuration about queries limit */ 080 public static final String QUERY_LIMIT_CONF_ID = "plugin.mobileapp.query.limit"; 081 082 /** The service manager */ 083 protected ServiceManager _manager; 084 085 /** The Ametys object resolver */ 086 private AmetysObjectResolver _resolver; 087 088 /** Queries Helper */ 089 private org.ametys.plugins.queriesdirectory.QueryHelper _queriesHelper; 090 091 /** SearchUI Model Extension Point */ 092 private SearchUIModelExtensionPoint _searchUiEP; 093 094 /** The helper to handler content types */ 095 private ContentTypesHelper _contentTypesHelper; 096 097 /** Content Searcher Factory */ 098 private ContentSearcherFactory _contentSearcherFactory; 099 100 private SiteManager _siteManager; 101 102 public void service(ServiceManager manager) throws ServiceException 103 { 104 _manager = manager; 105 } 106 107 /** 108 * Getter for AmetysObjectResolver to avoid problems at startup 109 * @return AmetysObjectResolver 110 */ 111 protected AmetysObjectResolver getResolver() 112 { 113 if (_resolver == null) 114 { 115 try 116 { 117 _resolver = (AmetysObjectResolver) _manager.lookup(AmetysObjectResolver.ROLE); 118 } 119 catch (ServiceException e) 120 { 121 getLogger().error("Error while retrieving AmetysObjectResolver", e); 122 } 123 } 124 return _resolver; 125 } 126 127 /** 128 * Getter for QueriesManager to avoid problems at startup 129 * @return QueriesManager 130 */ 131 protected org.ametys.plugins.queriesdirectory.QueryHelper getQueriesManager() 132 { 133 if (_queriesHelper == null) 134 { 135 try 136 { 137 _queriesHelper = (org.ametys.plugins.queriesdirectory.QueryHelper) _manager.lookup(org.ametys.plugins.queriesdirectory.QueryHelper.ROLE); 138 } 139 catch (ServiceException e) 140 { 141 getLogger().error("Error while retrieving QueriesManager", e); 142 } 143 } 144 return _queriesHelper; 145 } 146 147 /** 148 * Getter for SearchUIModelExtensionPoint to avoid problems at startup 149 * @return SearchUIModelExtensionPoint 150 */ 151 protected SearchUIModelExtensionPoint getSearchUIModelExtensionPoint() 152 { 153 if (_searchUiEP == null) 154 { 155 try 156 { 157 _searchUiEP = (SearchUIModelExtensionPoint) _manager.lookup(SearchUIModelExtensionPoint.ROLE); 158 } 159 catch (ServiceException e) 160 { 161 getLogger().error("Error while retrieving SearchUIModelExtensionPoint", e); 162 } 163 } 164 return _searchUiEP; 165 } 166 167 /** 168 * Getter for ContentTypesHelper to avoid problems at startup 169 * @return ContentTypesHelper 170 */ 171 protected ContentTypesHelper getContentTypesHelper() 172 { 173 if (_contentTypesHelper == null) 174 { 175 try 176 { 177 _contentTypesHelper = (ContentTypesHelper) _manager.lookup(ContentTypesHelper.ROLE); 178 } 179 catch (ServiceException e) 180 { 181 getLogger().error("Error while retrieving ContentTypesHelper", e); 182 } 183 } 184 return _contentTypesHelper; 185 } 186 187 /** 188 * Getter for SiteManager to avoid problems at startup 189 * @return SiteManager 190 */ 191 protected SiteManager getSiteManager() 192 { 193 if (_siteManager == null) 194 { 195 try 196 { 197 _siteManager = (SiteManager) _manager.lookup(SiteManager.ROLE); 198 } 199 catch (ServiceException e) 200 { 201 getLogger().error("Error while retrieving SiteManager", e); 202 } 203 } 204 return _siteManager; 205 } 206 207 /** 208 * Getter for ContentSearcherFactory to avoid problems at startup 209 * @return ContentSearcherFactory 210 */ 211 protected ContentSearcherFactory getContentSearcherFactory() 212 { 213 if (_contentSearcherFactory == null) 214 { 215 try 216 { 217 _contentSearcherFactory = (ContentSearcherFactory) _manager.lookup(ContentSearcherFactory.ROLE); 218 } 219 catch (ServiceException e) 220 { 221 getLogger().error("Error while retrieving SiteManager", e); 222 } 223 } 224 return _contentSearcherFactory; 225 } 226 227 /** 228 * List all queries available for the mobile app 229 * @param site the site. May be null 230 * @return a list of {@link Query} 231 */ 232 public List<Query> getQueries(Site site) 233 { 234 String queryContainerId = site != null ? site.getValue(QUERY_CONTAINER_SITE_CONF_ID) : null; 235 236 if (StringUtils.isBlank(queryContainerId)) 237 { 238 queryContainerId = Config.getInstance().getValue(QUERY_CONTAINER_CONF_ID); 239 } 240 241 AmetysObject ametysObject = getResolver().resolveById(queryContainerId); 242 return getQueries(ametysObject); 243 } 244 245 /** 246 * Get all queries available in a specific {@link QueryContainer} 247 * @param ametysObject a {@link QueryContainer} to parse, or directly a {@link Query} to return in a list 248 * @return a list containing all {@link Query} available in this {@link QueryContainer} 249 */ 250 public List<Query> getQueries(AmetysObject ametysObject) 251 { 252 getLogger().info("Fetch queries under query '{}'", ametysObject.getId()); 253 if (ametysObject instanceof QueryContainer) 254 { 255 QueryContainer queryContainer = (QueryContainer) ametysObject; 256 257 return queryContainer.getChildren().stream() 258 .map(q -> getQueries(q)) 259 .flatMap(List::stream) 260 .collect(Collectors.toList()); 261 } 262 else if (ametysObject instanceof Query) 263 { 264 getLogger().info("Query '{}' is not a container", ametysObject.getId()); 265 return ImmutableList.of((Query) ametysObject); 266 } 267 else 268 { 269 getLogger().info("Query '{}' is empty", ametysObject.getId()); 270 return Collections.EMPTY_LIST; 271 } 272 } 273 274 /** 275 * Execute a query, with a forced sort 276 * @param query the query to execute 277 * @param sort a list of sert elements, can contain lastValidation 278 * @param checkRights check the rights while executing the query 279 * @return the results of the query 280 */ 281 public AmetysObjectIterable<Content> executeQuery(Query query, List<Sort> sort, boolean checkRights) 282 { 283 getLogger().info("Execute query '{}', testing rights : '{}'", query.getId(), checkRights); 284 try 285 { 286 Long limit = Config.getInstance().getValue(QUERY_LIMIT_CONF_ID); 287 Map<String, Object> exportParams = getQueriesManager().getExportParams(query); 288 289 String model = getQueriesManager().getModelForQuery(exportParams); 290 291 if ("solr".equals(query.getType())) 292 { 293 String queryStr = getQueriesManager().getQueryStringForQuery(exportParams); 294 Set<String> contentTypeIds = getQueriesManager().getContentTypesForQuery(exportParams).stream().collect(Collectors.toSet()); 295 AmetysObjectIterable<Content> results = getContentSearcherFactory().create(contentTypeIds) 296 .setCheckRights(checkRights) 297 .withSort(sort) 298 .withLimits(0, limit.intValue()) 299 .search(queryStr); 300 getLogger().info("Query '{}' returned {} results", query.getId(), results.getSize()); 301 return results; 302 } 303 else if (Query.Type.SIMPLE.toString().equals(query.getType()) || Query.Type.ADVANCED.toString().equals(query.getType())) 304 { 305 Map<String, Object> values = getQueriesManager().getValuesForQuery(exportParams); 306 307 Map<String, Object> contextualParameters = getQueriesManager().getContextualParametersForQuery(exportParams); 308 String searchMode = getQueriesManager().getSearchModeForQuery(exportParams); 309 SearchUIModel uiModel = getSearchUIModelExtensionPoint().getExtension(model); 310 SearchUIModel wrappedUiModel = new SearchModelWrapper(uiModel, _manager, getLogger()); 311 312 SearchModelContentSearcher searcher = getContentSearcherFactory().create(wrappedUiModel); 313 AmetysObjectIterable<Content> results = searcher 314 .setCheckRights(checkRights) 315 .withLimits(0, limit.intValue()) 316 .withSort(sort) 317 .withSearchMode(searchMode) 318 .search(values, contextualParameters); 319 getLogger().info("Query '{}' returned {} results", query.getId(), results.getSize()); 320 return results; 321 } 322 } 323 catch (Exception e) 324 { 325 getLogger().error("Error during the execution of the query '" + query.getTitle() + "' (" + query.getId() + ")", e); 326 } 327 return null; 328 } 329 330 /** 331 * Execute all queries to return the list of the queries that return this object 332 * @param content the object to test 333 * @param site the site. May be null 334 * @return a list containing all the impacted queries 335 */ 336 public Set<Query> getQueriesForResult(AmetysObject content, Site site) 337 { 338 return getQueriesForResult(content.getId(), site); 339 } 340 341 /** 342 * Execute all queries to return the list of the queries that return this object 343 * @param contentId the content id 344 * @param site the site. May be null 345 * @return a list containing all the impacted queries 346 */ 347 public Set<Query> getQueriesForResult(String contentId, Site site) 348 { 349 Map<String, Set<Query>> queriesForResult = getQueriesForResult(List.of(contentId), site); 350 if (queriesForResult.containsKey(contentId)) 351 { 352 return queriesForResult.get(contentId); 353 } 354 else 355 { 356 return Collections.EMPTY_SET; 357 } 358 } 359 360 /** 361 * Execute all queries to return the list of the queries that return this object 362 * @param contents a list of object to test 363 * @param site the site. May be null 364 * @return a map containing, for each content, the list containing all the impacted queries 365 */ 366 public Map<String, Set<Query>> getQueriesForResult(List<String> contents, Site site) 367 { 368 Map<String, Set<Query>> result = new HashMap<>(); 369 List<Query> queries = getQueries(site); 370 371 getLogger().info("Searching for queries returning contents : {}", contents); 372 for (Query query : queries) 373 { 374 getLogger().info("Searching with query : {}", query.getId()); 375 try 376 { 377 List<Sort> sort = getSortProperty(query, queries.size() > 1); 378 379 AmetysObjectIterable<Content> searchResults = executeQuery(query, sort, false); 380 if (searchResults != null) 381 { 382 List<String> resultIds = searchResults.stream().map(c -> c.getId()).collect(Collectors.toList()); 383 384 for (String resultId : resultIds) 385 { 386 if (contents.contains(resultId)) 387 { 388 getLogger().info("Query : {} contains content : {}", query.getId(), resultId); 389 if (!result.containsKey(resultId)) 390 { 391 result.put(resultId, new HashSet<>()); 392 } 393 result.get(resultId).add(query); 394 } 395 } 396 } 397 } 398 catch (Exception e) 399 { 400 getLogger().error("Error during the execution of the query '" + query.getTitle() + "' (" + query.getId() + ")", e); 401 } 402 } 403 if (result.isEmpty()) 404 { 405 getLogger().info("No query found for contents '{}'", contents); 406 } 407 return result; 408 } 409 410 /** 411 * Generate a list of sort properties. 412 * If there are multiple queries, they have to be sorted first by date ASC. If they already are, the sort is used, otherwise a sort on lastValidation is used 413 * @param query the query that will be executed 414 * @param moreThanOne if false and the query already contain a sort, the original sort will be returned 415 * @return a list of sort, with the first one by date if moreThanOne is true 416 */ 417 public List<Sort> getSortProperty(Query query, boolean moreThanOne) 418 { 419 Map<String, Object> exportParams = getQueriesManager().getExportParams(query); 420 List<Sort> baseSort = getQueriesManager().getSortForQuery(exportParams); 421 if (!moreThanOne && baseSort != null && !baseSort.isEmpty()) 422 { 423 return baseSort; 424 } 425 else 426 { 427 if (baseSort != null && !baseSort.isEmpty()) 428 { 429 Sort firstSort = baseSort.get(0); 430 Set<String> specificFields = Set.of("creationDate", "lastValidation", "lastModified"); 431 if (specificFields.contains(firstSort.getField())) 432 { 433 firstSort.setOrder(Order.ASC); 434 return List.of(firstSort); 435 } 436 437 List<String> contentTypesForQuery = getQueriesManager().getContentTypesForQuery(exportParams); 438 if (contentTypesForQuery != null) 439 { 440 String[] contentTypeIdsAsArray = contentTypesForQuery.toArray(new String[contentTypesForQuery.size()]); 441 try 442 { 443 ModelItem modelItem = getContentTypesHelper().getModelItem(firstSort.getField(), contentTypeIdsAsArray, new String[0]); 444 445 String sortFileTypeId = modelItem.getType().getId(); 446 if (ModelItemTypeConstants.DATE_TYPE_ID.equals(sortFileTypeId) || ModelItemTypeConstants.DATETIME_TYPE_ID.equals(sortFileTypeId)) 447 { 448 firstSort.setOrder(Order.ASC); 449 return List.of(firstSort); 450 } 451 } 452 catch (UndefinedItemPathException e) 453 { 454 getLogger().warn("Error while fetching the model of the field '" + firstSort.getField() + "' for content types [" + String.join(", ", contentTypesForQuery) + "]", e); 455 } 456 } 457 } 458 Sort sort = new Sort("lastValidation", Sort.Order.ASC); 459 return List.of(sort); 460 } 461 } 462 463 /** 464 * Get a json object representing the content 465 * @param content tho content to parse 466 * @return a json map 467 */ 468 public Map<String, String> getDataForContent(Content content) 469 { 470 Map<String, String> result = new HashMap<>(); 471 // feed_id 472 // category_name 473 // date 474 // content_id 475 result.put("content_id", content.getId()); 476 if (content.hasValue("illustration/image")) 477 { 478 String imgUri = "illustration/image?contentId=" + content.getId(); 479 String image = ResolveURIComponent.resolveCroppedImage("attribute", imgUri, PostConstants.IMAGE_SIZE, PostConstants.IMAGE_SIZE, false, true); 480 result.put("image", image); 481 } 482 // title 483 result.put("title", content.getTitle()); 484 // content_url 485 if (content instanceof WebContent) 486 { 487 WebContent webContent = (WebContent) content; 488 Iterator<Page> pages = webContent.getReferencingPages().iterator(); 489 if (pages.hasNext()) 490 { 491 Page page = pages.next(); 492 String siteName = webContent.getSiteName(); 493 String url = getSiteManager().getSite(siteName).getUrl() + "/" + page.getSitemap().getName() + "/" + page.getPathInSitemap() + ".html"; 494 result.put("content_url", url); 495 } 496 497 } 498 return result; 499 } 500 501 /** 502 * Transform a ZonedDateTime, LocalDate or Date to an instant 503 * @param o1 the date to transform 504 * @param o2 a relative date, if it contains a timezone and the 1st does not, this timezone will bu used 505 * @return an Instant 506 */ 507 public Instant toInstant(Object o1, Object o2) 508 { 509 if (o1 instanceof ZonedDateTime) 510 { 511 return ((ZonedDateTime) o1).toInstant(); 512 } 513 else if (o1 instanceof LocalDate) 514 { 515 if (o2 instanceof ZonedDateTime) 516 { 517 LocalDate ld1 = (LocalDate) o1; 518 ZonedDateTime zdt2 = (ZonedDateTime) o2; 519 return ld1.atStartOfDay(zdt2.getZone()).toInstant(); 520 } 521 else 522 { 523 return ((LocalDate) o1).atStartOfDay(ZoneId.systemDefault()).toInstant(); 524 } 525 } 526 else if (o1 instanceof Date) 527 { 528 return ((Date) o1).toInstant(); 529 } 530 531 return null; 532 } 533 534 /** 535 * Get the date of the content, can be the date requested in the field, or by default the last validation date 536 * @param content the content to read 537 * @param field the field to check 538 * @return the iso formatted date 539 */ 540 public String getContentFormattedDate(Content content, String field) 541 { 542 String isoDate = null; 543 if (StringUtils.isNotBlank(field) && content.hasValue(field)) 544 { 545 Object value = content.getValue(field, true, null); 546 if (value instanceof Date) 547 { 548 isoDate = DateUtils.dateToString((Date) value); 549 } 550 else 551 { 552 Instant instant = toInstant(value, null); 553 if (instant != null) 554 { 555 Date date = new Date(instant.toEpochMilli()); 556 isoDate = DateUtils.dateToString(date); 557 } 558 } 559 } 560 else if ("lastModified".equals(field)) 561 { 562 // lastModified is not a content field 563 isoDate = DateUtils.zonedDateTimeToString(content.getLastModified()); 564 } 565 566 // If no date found, use the last validation date 567 if (StringUtils.isBlank(isoDate)) 568 { 569 isoDate = DateUtils.zonedDateTimeToString(content.getLastValidationDate()); 570 } 571 572 return isoDate; 573 } 574}