001/* 002 * Copyright 2016 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.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.stream.Stream; 027 028import javax.jcr.Node; 029import javax.jcr.RepositoryException; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.jackrabbit.util.Text; 037 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.right.RightManager; 041import org.ametys.core.right.RightManager.RightResult; 042import org.ametys.core.ui.Callable; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.util.DateUtils; 046import org.ametys.core.util.LambdaUtils; 047import org.ametys.plugins.core.user.UserHelper; 048import org.ametys.plugins.queriesdirectory.observation.ObservationConstants; 049import org.ametys.plugins.repository.AmetysObject; 050import org.ametys.plugins.repository.AmetysObjectIterable; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.AmetysRepositoryException; 053import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 054import org.ametys.plugins.repository.MovableAmetysObject; 055import org.ametys.plugins.repository.UnknownAmetysObjectException; 056import org.ametys.plugins.repository.jcr.NameHelper; 057import org.ametys.runtime.plugin.component.AbstractLogEnabled; 058 059import com.google.common.base.Predicates; 060import com.google.common.collect.ImmutableMap; 061 062/** 063 * DAO for manipulating queries 064 */ 065public class QueryDAO extends AbstractLogEnabled implements Serviceable, Component 066{ 067 /** The Avalon role */ 068 public static final String ROLE = QueryDAO.class.getName(); 069 070 /** The right id to handle query */ 071 public static final String QUERY_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Admin"; 072 073 /** The right id to handle query container */ 074 public static final String QUERY_CONTAINER_HANDLE_RIGHT_ID = "QueriesDirectory_Rights_Containers"; 075 076 /** The alias id of the root {@link QueryContainer} */ 077 public static final String ROOT_QUERY_CONTAINER_ID = "root"; 078 079 /** Propery key for read access */ 080 public static final String READ_ACCESS_PROPERTY = "canRead"; 081 /** Propery key for write access */ 082 public static final String WRITE_ACCESS_PROPERTY = "canWrite"; 083 /** Propery key for rename access */ 084 public static final String RENAME_ACCESS_PROPERTY = "canRename"; 085 /** Propery key for delete access */ 086 public static final String DELETE_ACCESS_PROPERTY = "canDelete"; 087 /** Propery key for edit rights access */ 088 public static final String EDIT_RIGHTS_ACCESS_PROPERTY = "canAssignRights"; 089 090 private static final String __PLUGIN_NODE_NAME = "queriesdirectory"; 091 092 /** The current user provider */ 093 protected CurrentUserProvider _userProvider; 094 095 /** The observation manager */ 096 protected ObservationManager _observationManager; 097 098 /** The Ametys object resolver */ 099 private AmetysObjectResolver _resolver; 100 101 private UserHelper _userHelper; 102 103 private RightManager _rightManager; 104 105 @Override 106 public void service(ServiceManager serviceManager) throws ServiceException 107 { 108 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 109 _userProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 110 _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE); 111 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 112 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 113 } 114 115 /** 116 * Get the root plugin storage object. 117 * @return the root plugin storage object. 118 * @throws AmetysRepositoryException if a repository error occurs. 119 */ 120 public QueryContainer getQueriesRootNode() throws AmetysRepositoryException 121 { 122 try 123 { 124 return _getOrCreateRootNode(); 125 } 126 catch (AmetysRepositoryException e) 127 { 128 throw new AmetysRepositoryException("Unable to get the queries root node", e); 129 } 130 } 131 132 private QueryContainer _getOrCreateRootNode() throws AmetysRepositoryException 133 { 134 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins"); 135 136 ModifiableTraversableAmetysObject pluginNode = (ModifiableTraversableAmetysObject) _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured"); 137 138 return (QueryContainer) _getOrCreateNode(pluginNode, "ametys:queries", QueryContainerFactory.QUERY_CONTAINER_NODETYPE); 139 } 140 141 private static AmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 142 { 143 AmetysObject definitionsNode; 144 if (parentNode.hasChild(nodeName)) 145 { 146 definitionsNode = parentNode.getChild(nodeName); 147 } 148 else 149 { 150 definitionsNode = parentNode.createChild(nodeName, nodeType); 151 parentNode.saveChanges(); 152 } 153 return definitionsNode; 154 } 155 156 /** 157 * Get queries' properties 158 * @param queryIds The ids of queries to retrieve 159 * @return The queries' properties 160 */ 161 @Callable 162 public Map<String, Object> getQueriesProperties(List<String> queryIds) 163 { 164 Map<String, Object> result = new HashMap<>(); 165 166 List<Map<String, Object>> queries = new LinkedList<>(); 167 List<Map<String, Object>> notAllowedQueries = new LinkedList<>(); 168 Set<String> unknownQueries = new HashSet<>(); 169 170 UserIdentity user = _userProvider.getUser(); 171 172 for (String id : queryIds) 173 { 174 try 175 { 176 Query query = _resolver.resolveById(id); 177 178 if (canRead(user, query)) 179 { 180 queries.add(getQueryProperties(query)); 181 } 182 else 183 { 184 notAllowedQueries.add(getQueryProperties(query)); 185 } 186 } 187 catch (UnknownAmetysObjectException e) 188 { 189 unknownQueries.add(id); 190 } 191 } 192 193 result.put("queries", queries); 194 result.put("unknownQueries", unknownQueries); 195 196 return result; 197 } 198 199 /** 200 * Get the query properties 201 * @param query The query 202 * @return The query properties 203 */ 204 public Map<String, Object> getQueryProperties (Query query) 205 { 206 Map<String, Object> infos = new HashMap<>(); 207 208 List<String> fullPath = new ArrayList<>(); 209 fullPath.add(query.getTitle()); 210 211 AmetysObject node = query.getParent(); 212 while (node instanceof QueryContainer 213 && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root 214 { 215 fullPath.add(0, node.getName()); 216 node = node.getParent(); 217 } 218 219 220 infos.put("isQuery", true); 221 infos.put("id", query.getId()); 222 infos.put("title", query.getTitle()); 223 infos.put("fullPath", String.join(" > ", fullPath)); 224 infos.put("type", query.getType()); 225 infos.put("description", query.getDescription()); 226 infos.put("documentation", query.getDocumentation()); 227 infos.put("author", _userHelper.user2json(query.getAuthor())); 228 infos.put("contributor", _userHelper.user2json(query.getContributor())); 229 infos.put("content", query.getContent()); 230 infos.put("lastModificationDate", DateUtils.zonedDateTimeToString(query.getLastModificationDate())); 231 infos.put("creationDate", DateUtils.zonedDateTimeToString(query.getCreationDate())); 232 233 UserIdentity currentUser = _userProvider.getUser(); 234 infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, query)); 235 infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, query)); 236 infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, query)); 237 infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, query)); 238 239 return infos; 240 } 241 242 /** 243 * Gets the ids of the path elements of a query or query container, i.e. the parent ids. 244 * <br>For instance, if the query path is 'a/b/c', then the result list will be ["id-of-a", "id-of-b", "id-of-c"] 245 * @param queryId The id of the query 246 * @return the ids of the path elements of a query 247 */ 248 @Callable 249 public List<String> getIdsOfPath(String queryId) 250 { 251 AmetysObject queryOrQueryContainer = _resolver.resolveById(queryId); 252 QueryContainer queriesRootNode = getQueriesRootNode(); 253 254 if (!(queryOrQueryContainer instanceof Query) && !(queryOrQueryContainer instanceof QueryContainer)) 255 { 256 throw new IllegalArgumentException("The given id is not a query nor a query container"); 257 } 258 259 List<String> pathElements = new ArrayList<>(); 260 QueryContainer current = queryOrQueryContainer.getParent(); 261 while (!queriesRootNode.equals(current)) 262 { 263 pathElements.add(0, current.getId()); 264 current = current.getParent(); 265 } 266 267 return pathElements; 268 } 269 270 /** 271 * Get the root container properties 272 * @return The root container properties 273 */ 274 @Callable 275 public Map<String, Object> getRootProperties() 276 { 277 return getQueryContainerProperties(getQueriesRootNode()); 278 } 279 280 /** 281 * Get the query container properties 282 * @param id The query container id. Can be {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 283 * @return The query container properties 284 */ 285 @Callable 286 public Map<String, Object> getQueryContainerProperties(String id) 287 { 288 return getQueryContainerProperties(_getQueryContainer(id)); 289 } 290 291 /** 292 * Get the query container properties 293 * @param queryContainer The query container 294 * @return The query container properties 295 */ 296 public Map<String, Object> getQueryContainerProperties(QueryContainer queryContainer) 297 { 298 Map<String, Object> infos = new HashMap<>(); 299 300 infos.put("isQuery", false); 301 infos.put("id", queryContainer.getId()); 302 infos.put("name", queryContainer.getName()); 303 infos.put("title", queryContainer.getName()); 304 infos.put("fullPath", _getFullPath(queryContainer)); 305 306 UserIdentity currentUser = _userProvider.getUser(); 307 308 infos.put(READ_ACCESS_PROPERTY, canRead(currentUser, queryContainer)); 309 infos.put(WRITE_ACCESS_PROPERTY, canWrite(currentUser, queryContainer)); // create container, add query 310 infos.put(RENAME_ACCESS_PROPERTY, canRename(currentUser, queryContainer)); // rename 311 infos.put(DELETE_ACCESS_PROPERTY, canDelete(currentUser, queryContainer)); // delete or move 312 infos.put(EDIT_RIGHTS_ACCESS_PROPERTY, canAssignRights(currentUser, queryContainer)); // edit rights 313 314 return infos; 315 } 316 317 private String _getFullPath(QueryContainer queryContainer) 318 { 319 List<String> fullPath = new ArrayList<>(); 320 fullPath.add(queryContainer.getName()); 321 322 AmetysObject node = queryContainer.getParent(); 323 while (node instanceof QueryContainer 324 && node.getParent() instanceof QueryContainer) // The parent must also be a container to avoid the root 325 { 326 fullPath.add(0, node.getName()); 327 node = node.getParent(); 328 } 329 return String.join(" > ", fullPath); 330 } 331 332 /** 333 * Filter queries on the server side 334 * @param rootNode The root node where to seek (to refresh subtrees) 335 * @param search Textual search 336 * @param ownerOnly Only queries of the owner will be returned 337 * @param requestType Only simple/advanced requests will be returned 338 * @param solrType Only Solr requests will be returned 339 * @param scriptType Only script requests will be returned 340 * @param formattingType Only formatting will be returned 341 * @return The list of query path 342 */ 343 @Callable 344 public List<String> filterQueries(String rootNode, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 345 { 346 List<String> matchingPaths = new ArrayList<>(); 347 348 _getMatchingQueries("root".equals(rootNode) ? getQueriesRootNode() : _resolver.resolveById(rootNode), 349 matchingPaths, StringUtils.stripAccents(search.toLowerCase()), ownerOnly, requestType, solrType, scriptType, formattingType); 350 351 return matchingPaths; 352 } 353 354 private void _getMatchingQueries(QueryContainer queryContainer, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 355 { 356 String containerName = StringUtils.stripAccents(queryContainer.getName().toLowerCase()); 357 if (StringUtils.isNotBlank(search) && containerName.contains(search)) 358 { 359 matchingPaths.add(_getQueryPath(queryContainer)); 360 } 361 362 if (!hasAnyReadableDescendant(_userProvider.getUser(), queryContainer)) 363 { 364 return; 365 } 366 367 for (AmetysObject child : queryContainer.getChildren()) 368 { 369 if (child instanceof QueryContainer childQueryContainer) 370 { 371 _getMatchingQueries(childQueryContainer, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 372 } 373 else if (child instanceof Query childQuery) 374 { 375 _getMatchingQuery(childQuery, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 376 } 377 } 378 } 379 380 private void _getMatchingQuery(Query query, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 381 { 382 String type = query.getType(); 383 384 if (!canRead(_userProvider.getUser(), query) 385 || formattingType && !"formatting".equals(type) 386 || requestType && !Query.Type.SIMPLE.toString().equals(type) && !Query.Type.ADVANCED.toString().equals(type) 387 || solrType && !"solr".equals(type) 388 || scriptType && !Query.Type.SCRIPT.toString().equals(type) 389 || ownerOnly && !query.getAuthor().equals(_userProvider.getUser())) 390 { 391 return; 392 } 393 394 String queryLabel = StringUtils.stripAccents(query.getTitle().toLowerCase()); 395 if (!queryLabel.contains(search)) 396 { 397 return; 398 } 399 400 // TODO tenir compte des droits 401 402 matchingPaths.add(_getQueryPath(query)); 403 } 404 405 private String _getQueryPath(AmetysObject queryOrContainer) 406 { 407 if (queryOrContainer instanceof Query 408 || queryOrContainer instanceof QueryContainer 409 && queryOrContainer.getParent() instanceof QueryContainer) 410 { 411 return _getQueryPath(queryOrContainer.getParent()) + "#" + queryOrContainer.getId(); 412 } 413 else 414 { 415 return "#root"; 416 } 417 } 418 419 /** 420 * Creates a new {@link Query} 421 * @param title The title of the query 422 * @param desc The description of the query 423 * @param documentation The documentation of the query 424 * @param type The type of the query 425 * @param content The content of the query 426 * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 427 * @return A result map 428 */ 429 @Callable 430 public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId) 431 { 432 Map<String, Object> results = new HashMap<>(); 433 434 QueryContainer queriesNode = _getQueryContainer(parentId); 435 436 if (!canWrite(_userProvider.getUser(), queriesNode)) 437 { 438 results.put("message", "not-allowed"); 439 return results; 440 } 441 442 String name = NameHelper.filterName(title); 443 444 // Find unique name 445 String uniqueName = name; 446 int index = 2; 447 while (queriesNode.hasChild(uniqueName)) 448 { 449 uniqueName = name + "-" + (index++); 450 } 451 452 Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE); 453 query.setTitle(title); 454 query.setDescription(desc); 455 query.setDocumentation(documentation); 456 query.setAuthor(_userProvider.getUser()); 457 query.setContributor(_userProvider.getUser()); 458 query.setType(type); 459 query.setContent(content); 460 query.setCreationDate(ZonedDateTime.now()); 461 query.setLastModificationDate(ZonedDateTime.now()); 462 463 queriesNode.saveChanges(); 464 465 results.put("id", query.getId()); 466 results.put("title", query.getTitle()); 467 results.put("content", query.getContent()); 468 469 return results; 470 } 471 472 /** 473 * Creates a new {@link QueryContainer} 474 * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 475 * @param name The desired name for the new {@link QueryContainer} 476 * @return A result map 477 */ 478 @Callable 479 public Map<String, Object> createQueryContainer(String parentId, String name) 480 { 481 QueryContainer parent = _getQueryContainer(parentId); 482 483 if (!canWrite(_userProvider.getUser(), parent)) 484 { 485 return ImmutableMap.of("message", "not-allowed"); 486 } 487 488 int index = 2; 489 String legalName = Text.escapeIllegalJcrChars(name); 490 String realName = legalName; 491 while (parent.hasChild(realName)) 492 { 493 realName = legalName + " (" + index + ")"; 494 index++; 495 } 496 497 QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE); 498 parent.saveChanges(); 499 500 return getQueryContainerProperties(createdChild); 501 } 502 503 /** 504 * Edits a {@link Query} 505 * @param id The id of the query 506 * @param title The title of the query 507 * @param desc The description of the query 508 * @param documentation The documentation of the query 509 * @return A result map 510 */ 511 @Callable 512 public Map<String, Object> updateQuery(String id, String title, String desc, String documentation) 513 { 514 Map<String, Object> results = new HashMap<>(); 515 516 Query query = _resolver.resolveById(id); 517 518 if (canWrite(_userProvider.getUser(), query)) 519 { 520 query.setTitle(title); 521 query.setDescription(desc); 522 query.setDocumentation(documentation); 523 query.setContributor(_userProvider.getUser()); 524 query.setLastModificationDate(ZonedDateTime.now()); 525 query.saveChanges(); 526 } 527 else 528 { 529 results.put("message", "not-allowed"); 530 } 531 532 results.put("id", query.getId()); 533 results.put("title", query.getTitle()); 534 535 return results; 536 } 537 538 /** 539 * Renames a {@link QueryContainer} 540 * @param id The id of the query container 541 * @param newName The new name of the container 542 * @return A result map 543 */ 544 @Callable 545 public Map<String, Object> renameQueryContainer(String id, String newName) 546 { 547 QueryContainer queryContainer = _resolver.resolveById(id); 548 549 UserIdentity currentUser = _userProvider.getUser(); 550 551 if (canRename(currentUser, queryContainer)) 552 { 553 String legalName = Text.escapeIllegalJcrChars(newName); 554 Node node = queryContainer.getNode(); 555 try 556 { 557 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 558 node.getSession().save(); 559 560 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 561 } 562 catch (RepositoryException e) 563 { 564 getLogger().error("Unable to rename query container '{}'", id, e); 565 return Map.of("message", "cannot-rename"); 566 } 567 } 568 else 569 { 570 return Map.of("message", "not-allowed"); 571 } 572 } 573 574 /** 575 * Moves a {@link Query} 576 * @param id The id of the query 577 * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 578 * @return A result map 579 */ 580 @Callable 581 public Map<String, Object> moveQuery(String id, String newParentId) 582 { 583 Map<String, Object> results = new HashMap<>(); 584 Query query = _resolver.resolveById(id); 585 QueryContainer queryContainer = _resolver.resolveById(newParentId); 586 587 if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer)) 588 { 589 if (!_move(query, newParentId)) 590 { 591 results.put("message", "cannot-move"); 592 } 593 } 594 else 595 { 596 results.put("message", "not-allowed"); 597 } 598 599 results.put("id", query.getId()); 600 return results; 601 } 602 603 /** 604 * Moves a {@link QueryContainer} 605 * @param id The id of the query container 606 * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 607 * @return A result map 608 */ 609 @Callable 610 public Map<String, Object> moveQueryContainer(String id, String newParentId) 611 { 612 QueryContainer queryContainer = _resolver.resolveById(id); 613 QueryContainer parentQueryContainer = _resolver.resolveById(newParentId); 614 UserIdentity currentUser = _userProvider.getUser(); 615 616 if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer)) 617 { 618 if (_move(queryContainer, newParentId)) 619 { 620 // returns updated properties 621 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 622 } 623 else 624 { 625 return Map.of("id", id, "message", "cannot-move"); 626 } 627 628 } 629 else 630 { 631 return Map.of("id", id, "message", "not-allowed"); 632 } 633 } 634 635 private boolean _move(MovableAmetysObject obj, String newParentId) 636 { 637 QueryContainer newParent = _getQueryContainer(newParentId); 638 if (obj.canMoveTo(newParent)) 639 { 640 try 641 { 642 obj.moveTo(newParent, false); 643 return true; 644 } 645 catch (AmetysRepositoryException e) 646 { 647 getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e); 648 } 649 } 650 return false; 651 } 652 653 private QueryContainer _getQueryContainer(String id) 654 { 655 QueryContainer container; 656 if (ROOT_QUERY_CONTAINER_ID.equals(id)) 657 { 658 container = getQueriesRootNode(); 659 } 660 else 661 { 662 container = _resolver.resolveById(id); 663 } 664 return container; 665 } 666 667 /** 668 * Saves a {@link Query} 669 * @param id The id of the query 670 * @param type The type of the query 671 * @param content The content of the query 672 * @return A result map 673 */ 674 @Callable 675 public Map<String, Object> saveQuery(String id, String type, String content) 676 { 677 Map<String, Object> results = new HashMap<>(); 678 679 Query query = _resolver.resolveById(id); 680 UserIdentity user = _userProvider.getUser(); 681 if (canWrite(user, query)) 682 { 683 query.setType(type); 684 query.setContent(content); 685 query.setContributor(user); 686 query.setLastModificationDate(ZonedDateTime.now()); 687 query.saveChanges(); 688 } 689 else 690 { 691 results.put("message", "not-allowed"); 692 } 693 694 results.put("id", query.getId()); 695 results.put("title", query.getTitle()); 696 results.put("content", query.getContent()); 697 698 return results; 699 } 700 701 /** 702 * Deletes {@link Query}(ies) 703 * @param ids The ids of the queries to delete 704 * @return A result map 705 */ 706 @Callable 707 public Map<String, Object> deleteQuery(List<String> ids) 708 { 709 Map<String, Object> results = new HashMap<>(); 710 711 List<String> deletedQueries = new ArrayList<>(); 712 List<String> unknownQueries = new ArrayList<>(); 713 List<String> notallowedQueries = new ArrayList<>(); 714 715 for (String id : ids) 716 { 717 try 718 { 719 Query query = _resolver.resolveById(id); 720 721 if (canDelete(_userProvider.getUser(), query)) 722 { 723 Map<String, Object> params = new HashMap<>(); 724 params.put(ObservationConstants.ARGS_QUERY_ID, query.getId()); 725 726 query.remove(); 727 query.saveChanges(); 728 deletedQueries.add(id); 729 730 _observationManager.notify(new Event(ObservationConstants.EVENT_QUERY_DELETED, _userProvider.getUser(), params)); 731 } 732 else 733 { 734 notallowedQueries.add(query.getTitle()); 735 } 736 } 737 catch (UnknownAmetysObjectException e) 738 { 739 unknownQueries.add(id); 740 getLogger().error("Unable to delete query. The query of id '{}' doesn't exist", id, e); 741 } 742 } 743 744 results.put("deletedQueries", deletedQueries); 745 results.put("notallowedQueries", notallowedQueries); 746 results.put("unknownQueries", unknownQueries); 747 748 return results; 749 } 750 751 /** 752 * Determines if application must warn before deleting the given {@link QueryContainer}s 753 * @param ids The {@link QueryContainer} ids 754 * @return <code>true</code> if application must warn 755 */ 756 @Callable 757 public boolean mustWarnBeforeDeletion(List<String> ids) 758 { 759 return ids.stream() 760 .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion)); 761 } 762 763 private boolean _mustWarnBeforeDeletion(String id) 764 { 765 QueryContainer container = _resolver.resolveById(id); 766 AmetysObjectIterable<Query> allQueries = getChildQueriesForAdministrator(container, false, List.of()); 767 return _containsNotOwnQueries(allQueries); 768 } 769 770 private boolean _containsNotOwnQueries(AmetysObjectIterable<Query> allQueries) 771 { 772 UserIdentity currentUser = _userProvider.getUser(); 773 return allQueries.stream() 774 .map(Query::getAuthor) 775 .anyMatch(Predicates.not(currentUser::equals)); 776 } 777 778 779 /** 780 * Deletes {@link QueryContainer}(s) 781 * @param ids The ids of the query containers to delete 782 * @return A result map 783 */ 784 @Callable 785 public Map<String, Object> deleteQueryContainer(List<String> ids) 786 { 787 Map<String, Object> results = new HashMap<>(); 788 789 List<Object> deletedQueryContainers = new ArrayList<>(); 790 List<String> unknownQueryContainers = new ArrayList<>(); 791 List<String> notallowedQueryContainers = new ArrayList<>(); 792 793 for (String id : ids) 794 { 795 try 796 { 797 QueryContainer queryContainer = _resolver.resolveById(id); 798 799 if (canDelete(_userProvider.getUser(), queryContainer)) 800 { 801 Map<String, Object> props = getQueryContainerProperties(queryContainer); 802 queryContainer.remove(); 803 queryContainer.saveChanges(); 804 deletedQueryContainers.add(props); 805 } 806 else 807 { 808 notallowedQueryContainers.add(queryContainer.getName()); 809 } 810 } 811 catch (UnknownAmetysObjectException e) 812 { 813 unknownQueryContainers.add(id); 814 getLogger().error("Unable to delete query container. The query container of id '{}' doesn't exist", id, e); 815 } 816 } 817 818 results.put("deletedQueryContainers", deletedQueryContainers); 819 results.put("notallowedQueryContainers", notallowedQueryContainers); 820 results.put("unknownQueryContainers", unknownQueryContainers); 821 822 return results; 823 } 824 825 /** 826 * Gets all queries for administrator for given parent 827 * @param parent The {@link QueryContainer}, defining the context from which getting children 828 * @param onlyDirect <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 829 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 830 * @return all queries for administrator for given parent 831 */ 832 public AmetysObjectIterable<Query> getChildQueriesForAdministrator(QueryContainer parent, boolean onlyDirect, List<String> acceptedTypes) 833 { 834 return _resolver.query(QueryHelper.getXPathForQueriesForAdministrator(parent, onlyDirect, acceptedTypes)); 835 } 836 837 /** 838 * Gets all queries in READ access for given parent 839 * @param parent The {@link QueryContainer}, defining the context from which getting children 840 * @param onlyDirect <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 841 * @param user The user 842 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 843 * @return all queries in READ access for given parent 844 */ 845 public Stream<Query> getChildQueriesInReadAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 846 { 847 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 848 .stream() 849 .filter(Query.class::isInstance) 850 .map(obj -> (Query) obj) 851 .filter(query -> canRead(user, query)); 852 } 853 854 /** 855 * Determine if user has read access on a query 856 * @param userIdentity the user 857 * @param query the query 858 * @return true if the user have read rights on a query 859 */ 860 public boolean canRead(UserIdentity userIdentity, Query query) 861 { 862 return _rightManager.hasReadAccess(userIdentity, query) || canWrite(userIdentity, query); 863 } 864 865 /** 866 * Determines if the user has read access on a query container 867 * @param userIdentity the user 868 * @param queryContainer the query container 869 * @return true if the user has read access on the query container 870 */ 871 public boolean canRead(UserIdentity userIdentity, QueryContainer queryContainer) 872 { 873 return _rightManager.hasReadAccess(userIdentity, queryContainer) || canWrite(userIdentity, queryContainer); 874 } 875 876 /** 877 * Gets all queries in WRITE access for given parent 878 * @param parent The {@link QueryContainer}, defining the context from which getting children 879 * @param onlyDirect <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 880 * @param user The user 881 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 882 * @return all queries in WRITE access for given parent 883 */ 884 public Stream<Query> getChildQueriesInWriteAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 885 { 886 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 887 .stream() 888 .filter(Query.class::isInstance) 889 .map(obj -> (Query) obj) 890 .filter(query -> canWrite(user, query)); 891 } 892 893 /** 894 * Determines if the user has write access on a query 895 * @param userIdentity the user 896 * @param query the query 897 * @return true if the user has write access on query 898 */ 899 public boolean canWrite(UserIdentity userIdentity, Query query) 900 { 901 return _rightManager.hasRight(userIdentity, QUERY_HANDLE_RIGHT_ID, query) == RightResult.RIGHT_ALLOW; 902 } 903 904 /** 905 * Determines if the user can delete a query 906 * @param userIdentity the user 907 * @param query the query 908 * @return true if the user can delete the query 909 */ 910 public boolean canDelete(UserIdentity userIdentity, Query query) 911 { 912 return canWrite(userIdentity, query) && canWrite(userIdentity, (QueryContainer) query.getParent()); 913 } 914 915 /** 916 * Determines if the user can rename a query container 917 * @param userIdentity the user 918 * @param queryContainer the query container 919 * @return true if the user can delete the query 920 */ 921 public boolean canRename(UserIdentity userIdentity, QueryContainer queryContainer) 922 { 923 return !_isRoot(queryContainer) && canWrite(userIdentity, queryContainer) && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()); 924 } 925 926 /** 927 * Determines if the user can delete a query container 928 * @param userIdentity the user 929 * @param queryContainer the query container 930 * @return true if the user can delete the query container 931 */ 932 public boolean canDelete(UserIdentity userIdentity, QueryContainer queryContainer) 933 { 934 return !_isRoot(queryContainer) // is not root 935 && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()) // has write access on parent 936 && canWrite(userIdentity, queryContainer, true); // has write access on itselft and each descendant 937 } 938 939 /** 940 * Determines if the query container is the root node 941 * @param queryContainer the query container 942 * @return true if is root 943 */ 944 protected boolean _isRoot(QueryContainer queryContainer) 945 { 946 return getQueriesRootNode().equals(queryContainer); 947 } 948 949 /** 950 * Gets all queries in WRITE access for given parent 951 * @param parent The {@link QueryContainer}, defining the context from which getting children 952 * @param onlyDirect <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 953 * @param user The user 954 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 955 * @return all queries in WRITE access for given parent 956 */ 957 public Stream<Query> getChildQueriesInRightAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 958 { 959 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 960 .stream() 961 .filter(Query.class::isInstance) 962 .map(obj -> (Query) obj) 963 .filter(query -> canAssignRights(user, query)); 964 } 965 966 /** 967 * Check if a user can edit rights on a query 968 * @param userIdentity the user 969 * @param query the query 970 * @return true if the user can edit rights on a query 971 */ 972 public boolean canAssignRights(UserIdentity userIdentity, Query query) 973 { 974 return canWrite(userIdentity, query) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 975 } 976 977 /** 978 * Check if a user has creation rights on a query container 979 * @param userIdentity the user identity 980 * @param queryContainer the query container 981 * @return true if the user has creation rights on a query container 982 */ 983 public boolean canCreate(UserIdentity userIdentity, QueryContainer queryContainer) 984 { 985 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, "/cms") == RightResult.RIGHT_ALLOW; 986 } 987 988 989 /** 990 * Check if a user has write access on a query container 991 * @param userIdentity the user identity 992 * @param queryContainer the query container 993 * @return true if the user has write access on the a query container 994 */ 995 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer) 996 { 997 return canWrite(userIdentity, queryContainer, false); 998 } 999 1000 /** 1001 * Check if a user has write access on a query container 1002 * @param userIdentity the user user identity 1003 * @param queryContainer the query container 1004 * @param recursively true to check write access on all descendants recursively 1005 * @return true if the user has write access on the a query container 1006 */ 1007 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer, boolean recursively) 1008 { 1009 boolean hasRight = _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, queryContainer) == RightResult.RIGHT_ALLOW; 1010 if (!hasRight) 1011 { 1012 return false; 1013 } 1014 1015 if (recursively) 1016 { 1017 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1018 { 1019 for (AmetysObject child : children) 1020 { 1021 if (child instanceof QueryContainer) 1022 { 1023 hasRight = hasRight && canWrite(userIdentity, (QueryContainer) child, true); 1024 } 1025 else if (child instanceof Query) 1026 { 1027 hasRight = hasRight && canWrite(userIdentity, (Query) child); 1028 } 1029 1030 if (!hasRight) 1031 { 1032 return false; 1033 } 1034 } 1035 } 1036 } 1037 1038 return hasRight; 1039 } 1040 1041 /** 1042 * Check if a user can edit rights on a query container 1043 * @param userIdentity the user 1044 * @param queryContainer the query container 1045 * @return true if the user can edit rights on a query 1046 */ 1047 public boolean canAssignRights(UserIdentity userIdentity, QueryContainer queryContainer) 1048 { 1049 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 1050 } 1051 1052 /** 1053 * Gets all query containers for given parent 1054 * @param parent The {@link QueryContainer}, defining the context from which getting children 1055 * @return all query containers for given parent 1056 */ 1057 public AmetysObjectIterable<QueryContainer> getChildQueryContainers(QueryContainer parent) 1058 { 1059 return _resolver.query(QueryHelper.getXPathForQueryContainers(parent)); 1060 } 1061 1062 /** 1063 * Check if a folder have a descendant in read access for a given user 1064 * @param userIdentity the user 1065 * @param queryContainer the query container 1066 * @return true if the user have read right for at least one child of this container 1067 */ 1068 public boolean hasAnyReadableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1069 { 1070 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1071 { 1072 for (AmetysObject child : children) 1073 { 1074 if (child instanceof QueryContainer) 1075 { 1076 if (canRead(userIdentity, (QueryContainer) child) || hasAnyReadableDescendant(userIdentity, (QueryContainer) child)) 1077 { 1078 return true; 1079 } 1080 } 1081 else if (child instanceof Query && canRead(userIdentity, (Query) child)) 1082 { 1083 return true; 1084 } 1085 } 1086 } 1087 1088 return false; 1089 } 1090 1091 /** 1092 * Check if a query container have descendant in write access for a given user 1093 * @param userIdentity the user identity 1094 * @param queryContainer the query container 1095 * @return true if the user have write right for at least one child of this container 1096 */ 1097 public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1098 { 1099 boolean canWrite = false; 1100 1101 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1102 { 1103 for (AmetysObject child : children) 1104 { 1105 if (child instanceof QueryContainer) 1106 { 1107 if (canWrite(userIdentity, (QueryContainer) child) || hasAnyWritableDescendant(userIdentity, (QueryContainer) child)) 1108 { 1109 return true; 1110 } 1111 } 1112 else if (child instanceof Query && canWrite(userIdentity, (Query) child)) 1113 { 1114 return true; 1115 } 1116 } 1117 } 1118 1119 return canWrite; 1120 } 1121 1122 /** 1123 * Check if a folder have descendant in right assignment access for a given user 1124 * @param userIdentity the user identity 1125 * @param queryContainer the query container 1126 * @return true if the user have right assignment right for at least one child of this container 1127 */ 1128 public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1129 { 1130 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1131 { 1132 for (AmetysObject child : children) 1133 { 1134 if (child instanceof QueryContainer) 1135 { 1136 if (canAssignRights(userIdentity, (QueryContainer) child) || hasAnyAssignableDescendant(userIdentity, (QueryContainer) child)) 1137 { 1138 return true; 1139 } 1140 } 1141 else if (child instanceof Query) 1142 { 1143 if (canAssignRights(userIdentity, (Query) child)) 1144 { 1145 return true; 1146 } 1147 } 1148 } 1149 return false; 1150 } 1151 } 1152}