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 if (StringUtils.isBlank(search)) 357 { 358 return; 359 } 360 361 String containerName = StringUtils.stripAccents(queryContainer.getName().toLowerCase()); 362 if (containerName.contains(search)) 363 { 364 matchingPaths.add(_getQueryPath(queryContainer)); 365 } 366 367 if (!hasAnyReadableDescendant(_userProvider.getUser(), queryContainer)) 368 { 369 return; 370 } 371 372 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 373 { 374 for (AmetysObject child : children) 375 { 376 if (child instanceof QueryContainer childQueryContainer) 377 { 378 _getMatchingQueries(childQueryContainer, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 379 } 380 else if (child instanceof Query childQuery) 381 { 382 _getMatchingQuery(childQuery, matchingPaths, search, ownerOnly, requestType, solrType, scriptType, formattingType); 383 } 384 } 385 } 386 } 387 388 private void _getMatchingQuery(Query query, List<String> matchingPaths, String search, boolean ownerOnly, boolean requestType, boolean solrType, boolean scriptType, boolean formattingType) 389 { 390 String type = query.getType(); 391 392 if (!canRead(_userProvider.getUser(), query) 393 || formattingType && !"formatting".equals(type) 394 || requestType && !Query.Type.SIMPLE.toString().equals(type) && !Query.Type.ADVANCED.toString().equals(type) 395 || solrType && !"solr".equals(type) 396 || scriptType && !Query.Type.SCRIPT.toString().equals(type) 397 || ownerOnly && !query.getAuthor().equals(_userProvider.getUser())) 398 { 399 return; 400 } 401 402 if (_contains(query, search)) 403 { 404 matchingPaths.add(_getQueryPath(query)); 405 } 406 } 407 408 /** 409 * Check it the query contains the search string in its title, content, description or documentation 410 */ 411 private boolean _contains(Query query, String search) 412 { 413 if (StringUtils.stripAccents(query.getTitle().toLowerCase()).contains(search)) 414 { 415 return true; 416 } 417 418 if (StringUtils.stripAccents(query.getContent().toLowerCase()).contains(search)) 419 { 420 return true; 421 } 422 423 if (StringUtils.stripAccents(query.getDescription().toLowerCase()).contains(search)) 424 { 425 return true; 426 } 427 return false; 428 } 429 430 private String _getQueryPath(AmetysObject queryOrContainer) 431 { 432 if (queryOrContainer instanceof Query 433 || queryOrContainer instanceof QueryContainer 434 && queryOrContainer.getParent() instanceof QueryContainer) 435 { 436 return _getQueryPath(queryOrContainer.getParent()) + "#" + queryOrContainer.getId(); 437 } 438 else 439 { 440 return "#root"; 441 } 442 } 443 444 /** 445 * Creates a new {@link Query} 446 * @param title The title of the query 447 * @param desc The description of the query 448 * @param documentation The documentation of the query 449 * @param type The type of the query 450 * @param content The content of the query 451 * @param parentId The id of the parent of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 452 * @return A result map 453 */ 454 @Callable 455 public Map<String, Object> createQuery(String title, String desc, String documentation, String type, String content, String parentId) 456 { 457 Map<String, Object> results = new HashMap<>(); 458 459 QueryContainer queriesNode = _getQueryContainer(parentId); 460 461 if (!canWrite(_userProvider.getUser(), queriesNode)) 462 { 463 results.put("message", "not-allowed"); 464 return results; 465 } 466 467 String name = NameHelper.filterName(title); 468 469 // Find unique name 470 String uniqueName = name; 471 int index = 2; 472 while (queriesNode.hasChild(uniqueName)) 473 { 474 uniqueName = name + "-" + (index++); 475 } 476 477 Query query = queriesNode.createChild(uniqueName, QueryFactory.QUERY_NODETYPE); 478 query.setTitle(title); 479 query.setDescription(desc); 480 query.setDocumentation(documentation); 481 query.setAuthor(_userProvider.getUser()); 482 query.setContributor(_userProvider.getUser()); 483 query.setType(type); 484 query.setContent(content); 485 query.setCreationDate(ZonedDateTime.now()); 486 query.setLastModificationDate(ZonedDateTime.now()); 487 488 queriesNode.saveChanges(); 489 490 results.put("id", query.getId()); 491 results.put("title", query.getTitle()); 492 results.put("content", query.getContent()); 493 494 return results; 495 } 496 497 /** 498 * Creates a new {@link QueryContainer} 499 * @param parentId The id of the parent. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 500 * @param name The desired name for the new {@link QueryContainer} 501 * @return A result map 502 */ 503 @Callable 504 public Map<String, Object> createQueryContainer(String parentId, String name) 505 { 506 QueryContainer parent = _getQueryContainer(parentId); 507 508 if (!canWrite(_userProvider.getUser(), parent)) 509 { 510 return ImmutableMap.of("message", "not-allowed"); 511 } 512 513 int index = 2; 514 String legalName = Text.escapeIllegalJcrChars(name); 515 String realName = legalName; 516 while (parent.hasChild(realName)) 517 { 518 realName = legalName + " (" + index + ")"; 519 index++; 520 } 521 522 QueryContainer createdChild = parent.createChild(realName, QueryContainerFactory.QUERY_CONTAINER_NODETYPE); 523 parent.saveChanges(); 524 525 return getQueryContainerProperties(createdChild); 526 } 527 528 /** 529 * Edits a {@link Query} 530 * @param id The id of the query 531 * @param title The title of the query 532 * @param desc The description of the query 533 * @param documentation The documentation of the query 534 * @return A result map 535 */ 536 @Callable 537 public Map<String, Object> updateQuery(String id, String title, String desc, String documentation) 538 { 539 Map<String, Object> results = new HashMap<>(); 540 541 Query query = _resolver.resolveById(id); 542 543 if (canWrite(_userProvider.getUser(), query)) 544 { 545 query.setTitle(title); 546 query.setDescription(desc); 547 query.setDocumentation(documentation); 548 query.setContributor(_userProvider.getUser()); 549 query.setLastModificationDate(ZonedDateTime.now()); 550 query.saveChanges(); 551 } 552 else 553 { 554 results.put("message", "not-allowed"); 555 } 556 557 results.put("id", query.getId()); 558 results.put("title", query.getTitle()); 559 560 return results; 561 } 562 563 /** 564 * Renames a {@link QueryContainer} 565 * @param id The id of the query container 566 * @param newName The new name of the container 567 * @return A result map 568 */ 569 @Callable 570 public Map<String, Object> renameQueryContainer(String id, String newName) 571 { 572 QueryContainer queryContainer = _resolver.resolveById(id); 573 574 UserIdentity currentUser = _userProvider.getUser(); 575 576 if (canRename(currentUser, queryContainer)) 577 { 578 String legalName = Text.escapeIllegalJcrChars(newName); 579 Node node = queryContainer.getNode(); 580 try 581 { 582 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 583 node.getSession().save(); 584 585 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 586 } 587 catch (RepositoryException e) 588 { 589 getLogger().error("Unable to rename query container '{}'", id, e); 590 return Map.of("message", "cannot-rename"); 591 } 592 } 593 else 594 { 595 return Map.of("message", "not-allowed"); 596 } 597 } 598 599 /** 600 * Moves a {@link Query} 601 * @param id The id of the query 602 * @param newParentId The id of the new parent container of the query. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 603 * @return A result map 604 */ 605 @Callable 606 public Map<String, Object> moveQuery(String id, String newParentId) 607 { 608 Map<String, Object> results = new HashMap<>(); 609 Query query = _resolver.resolveById(id); 610 QueryContainer queryContainer = _resolver.resolveById(newParentId); 611 612 if (canDelete(_userProvider.getUser(), query) && canWrite(_userProvider.getUser(), queryContainer)) 613 { 614 if (!_move(query, newParentId)) 615 { 616 results.put("message", "cannot-move"); 617 } 618 } 619 else 620 { 621 results.put("message", "not-allowed"); 622 } 623 624 results.put("id", query.getId()); 625 return results; 626 } 627 628 /** 629 * Moves a {@link QueryContainer} 630 * @param id The id of the query container 631 * @param newParentId The id of the new parent container of the query container. Use {@link #ROOT_QUERY_CONTAINER_ID} for the root container. 632 * @return A result map 633 */ 634 @Callable 635 public Map<String, Object> moveQueryContainer(String id, String newParentId) 636 { 637 QueryContainer queryContainer = _resolver.resolveById(id); 638 QueryContainer parentQueryContainer = _resolver.resolveById(newParentId); 639 UserIdentity currentUser = _userProvider.getUser(); 640 641 if (canDelete(currentUser, queryContainer) && canWrite(currentUser, parentQueryContainer)) 642 { 643 if (_move(queryContainer, newParentId)) 644 { 645 // returns updated properties 646 return getQueryContainerProperties((QueryContainer) _resolver.resolveById(id)); 647 } 648 else 649 { 650 return Map.of("id", id, "message", "cannot-move"); 651 } 652 653 } 654 else 655 { 656 return Map.of("id", id, "message", "not-allowed"); 657 } 658 } 659 660 private boolean _move(MovableAmetysObject obj, String newParentId) 661 { 662 QueryContainer newParent = _getQueryContainer(newParentId); 663 if (obj.canMoveTo(newParent)) 664 { 665 try 666 { 667 obj.moveTo(newParent, false); 668 return true; 669 } 670 catch (AmetysRepositoryException e) 671 { 672 getLogger().error("Unable to move '{}' to query container '{}'", obj.getId(), newParentId, e); 673 } 674 } 675 return false; 676 } 677 678 private QueryContainer _getQueryContainer(String id) 679 { 680 QueryContainer container; 681 if (ROOT_QUERY_CONTAINER_ID.equals(id)) 682 { 683 container = getQueriesRootNode(); 684 } 685 else 686 { 687 container = _resolver.resolveById(id); 688 } 689 return container; 690 } 691 692 /** 693 * Saves a {@link Query} 694 * @param id The id of the query 695 * @param type The type of the query 696 * @param content The content of the query 697 * @return A result map 698 */ 699 @Callable 700 public Map<String, Object> saveQuery(String id, String type, String content) 701 { 702 Map<String, Object> results = new HashMap<>(); 703 704 Query query = _resolver.resolveById(id); 705 UserIdentity user = _userProvider.getUser(); 706 if (canWrite(user, query)) 707 { 708 query.setType(type); 709 query.setContent(content); 710 query.setContributor(user); 711 query.setLastModificationDate(ZonedDateTime.now()); 712 query.saveChanges(); 713 } 714 else 715 { 716 results.put("message", "not-allowed"); 717 } 718 719 results.put("id", query.getId()); 720 results.put("title", query.getTitle()); 721 results.put("content", query.getContent()); 722 723 return results; 724 } 725 726 /** 727 * Deletes {@link Query}(ies) 728 * @param ids The ids of the queries to delete 729 * @return A result map 730 */ 731 @Callable 732 public Map<String, Object> deleteQuery(List<String> ids) 733 { 734 Map<String, Object> results = new HashMap<>(); 735 736 List<String> deletedQueries = new ArrayList<>(); 737 List<String> unknownQueries = new ArrayList<>(); 738 List<String> notallowedQueries = new ArrayList<>(); 739 740 for (String id : ids) 741 { 742 try 743 { 744 Query query = _resolver.resolveById(id); 745 746 if (canDelete(_userProvider.getUser(), query)) 747 { 748 Map<String, Object> params = new HashMap<>(); 749 params.put(ObservationConstants.ARGS_QUERY_ID, query.getId()); 750 751 query.remove(); 752 query.saveChanges(); 753 deletedQueries.add(id); 754 755 _observationManager.notify(new Event(ObservationConstants.EVENT_QUERY_DELETED, _userProvider.getUser(), params)); 756 } 757 else 758 { 759 notallowedQueries.add(query.getTitle()); 760 } 761 } 762 catch (UnknownAmetysObjectException e) 763 { 764 unknownQueries.add(id); 765 getLogger().error("Unable to delete query. The query of id '{}' doesn't exist", id, e); 766 } 767 } 768 769 results.put("deletedQueries", deletedQueries); 770 results.put("notallowedQueries", notallowedQueries); 771 results.put("unknownQueries", unknownQueries); 772 773 return results; 774 } 775 776 /** 777 * Determines if application must warn before deleting the given {@link QueryContainer}s 778 * @param ids The {@link QueryContainer} ids 779 * @return <code>true</code> if application must warn 780 */ 781 @Callable 782 public boolean mustWarnBeforeDeletion(List<String> ids) 783 { 784 return ids.stream() 785 .anyMatch(LambdaUtils.wrapPredicate(this::_mustWarnBeforeDeletion)); 786 } 787 788 private boolean _mustWarnBeforeDeletion(String id) 789 { 790 QueryContainer container = _resolver.resolveById(id); 791 AmetysObjectIterable<Query> allQueries = getChildQueriesForAdministrator(container, false, List.of()); 792 return _containsNotOwnQueries(allQueries); 793 } 794 795 private boolean _containsNotOwnQueries(AmetysObjectIterable<Query> allQueries) 796 { 797 UserIdentity currentUser = _userProvider.getUser(); 798 return allQueries.stream() 799 .map(Query::getAuthor) 800 .anyMatch(Predicates.not(currentUser::equals)); 801 } 802 803 804 /** 805 * Deletes {@link QueryContainer}(s) 806 * @param ids The ids of the query containers to delete 807 * @return A result map 808 */ 809 @Callable 810 public Map<String, Object> deleteQueryContainer(List<String> ids) 811 { 812 Map<String, Object> results = new HashMap<>(); 813 814 List<Object> deletedQueryContainers = new ArrayList<>(); 815 List<String> unknownQueryContainers = new ArrayList<>(); 816 List<String> notallowedQueryContainers = new ArrayList<>(); 817 818 for (String id : ids) 819 { 820 try 821 { 822 QueryContainer queryContainer = _resolver.resolveById(id); 823 824 if (canDelete(_userProvider.getUser(), queryContainer)) 825 { 826 Map<String, Object> props = getQueryContainerProperties(queryContainer); 827 queryContainer.remove(); 828 queryContainer.saveChanges(); 829 deletedQueryContainers.add(props); 830 } 831 else 832 { 833 notallowedQueryContainers.add(queryContainer.getName()); 834 } 835 } 836 catch (UnknownAmetysObjectException e) 837 { 838 unknownQueryContainers.add(id); 839 getLogger().error("Unable to delete query container. The query container of id '{}' doesn't exist", id, e); 840 } 841 } 842 843 results.put("deletedQueryContainers", deletedQueryContainers); 844 results.put("notallowedQueryContainers", notallowedQueryContainers); 845 results.put("unknownQueryContainers", unknownQueryContainers); 846 847 return results; 848 } 849 850 /** 851 * Gets all queries for administrator for given parent 852 * @param parent The {@link QueryContainer}, defining the context from which getting children 853 * @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 854 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 855 * @return all queries for administrator for given parent 856 */ 857 public AmetysObjectIterable<Query> getChildQueriesForAdministrator(QueryContainer parent, boolean onlyDirect, List<String> acceptedTypes) 858 { 859 return _resolver.query(QueryHelper.getXPathForQueriesForAdministrator(parent, onlyDirect, acceptedTypes)); 860 } 861 862 /** 863 * Gets all queries in READ access for given parent 864 * @param parent The {@link QueryContainer}, defining the context from which getting children 865 * @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 866 * @param user The user 867 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 868 * @return all queries in READ access for given parent 869 */ 870 public Stream<Query> getChildQueriesInReadAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 871 { 872 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 873 .stream() 874 .filter(Query.class::isInstance) 875 .map(obj -> (Query) obj) 876 .filter(query -> canRead(user, query)); 877 } 878 879 /** 880 * Determine if user has read access on a query 881 * @param userIdentity the user 882 * @param query the query 883 * @return true if the user have read rights on a query 884 */ 885 public boolean canRead(UserIdentity userIdentity, Query query) 886 { 887 return _rightManager.hasReadAccess(userIdentity, query) || canWrite(userIdentity, query); 888 } 889 890 /** 891 * Determines if the user has read access on a query container 892 * @param userIdentity the user 893 * @param queryContainer the query container 894 * @return true if the user has read access on the query container 895 */ 896 public boolean canRead(UserIdentity userIdentity, QueryContainer queryContainer) 897 { 898 return _rightManager.hasReadAccess(userIdentity, queryContainer) || canWrite(userIdentity, queryContainer); 899 } 900 901 /** 902 * Gets all queries in WRITE access for given parent 903 * @param parent The {@link QueryContainer}, defining the context from which getting children 904 * @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 905 * @param user The user 906 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 907 * @return all queries in WRITE access for given parent 908 */ 909 public Stream<Query> getChildQueriesInWriteAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 910 { 911 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 912 .stream() 913 .filter(Query.class::isInstance) 914 .map(obj -> (Query) obj) 915 .filter(query -> canWrite(user, query)); 916 } 917 918 /** 919 * Determines if the user has write access on a query 920 * @param userIdentity the user 921 * @param query the query 922 * @return true if the user has write access on query 923 */ 924 public boolean canWrite(UserIdentity userIdentity, Query query) 925 { 926 return _rightManager.hasRight(userIdentity, QUERY_HANDLE_RIGHT_ID, query) == RightResult.RIGHT_ALLOW; 927 } 928 929 /** 930 * Determines if the user can delete a query 931 * @param userIdentity the user 932 * @param query the query 933 * @return true if the user can delete the query 934 */ 935 public boolean canDelete(UserIdentity userIdentity, Query query) 936 { 937 return canWrite(userIdentity, query) && canWrite(userIdentity, (QueryContainer) query.getParent()); 938 } 939 940 /** 941 * Determines if the user can rename a query container 942 * @param userIdentity the user 943 * @param queryContainer the query container 944 * @return true if the user can delete the query 945 */ 946 public boolean canRename(UserIdentity userIdentity, QueryContainer queryContainer) 947 { 948 return !_isRoot(queryContainer) && canWrite(userIdentity, queryContainer) && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()); 949 } 950 951 /** 952 * Determines if the user can delete a query container 953 * @param userIdentity the user 954 * @param queryContainer the query container 955 * @return true if the user can delete the query container 956 */ 957 public boolean canDelete(UserIdentity userIdentity, QueryContainer queryContainer) 958 { 959 return !_isRoot(queryContainer) // is not root 960 && canWrite(userIdentity, (QueryContainer) queryContainer.getParent()) // has write access on parent 961 && canWrite(userIdentity, queryContainer, true); // has write access on itselft and each descendant 962 } 963 964 /** 965 * Determines if the query container is the root node 966 * @param queryContainer the query container 967 * @return true if is root 968 */ 969 protected boolean _isRoot(QueryContainer queryContainer) 970 { 971 return getQueriesRootNode().equals(queryContainer); 972 } 973 974 /** 975 * Gets all queries in WRITE access for given parent 976 * @param parent The {@link QueryContainer}, defining the context from which getting children 977 * @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 978 * @param user The user 979 * @param acceptedTypes The type of queries. Can be null or empty to accept all types. 980 * @return all queries in WRITE access for given parent 981 */ 982 public Stream<Query> getChildQueriesInRightAccess(QueryContainer parent, boolean onlyDirect, UserIdentity user, List<String> acceptedTypes) 983 { 984 return _resolver.query(QueryHelper.getXPathForQueries(parent, onlyDirect, acceptedTypes)) 985 .stream() 986 .filter(Query.class::isInstance) 987 .map(obj -> (Query) obj) 988 .filter(query -> canAssignRights(user, query)); 989 } 990 991 /** 992 * Check if a user can edit rights on a query 993 * @param userIdentity the user 994 * @param query the query 995 * @return true if the user can edit rights on a query 996 */ 997 public boolean canAssignRights(UserIdentity userIdentity, Query query) 998 { 999 return canWrite(userIdentity, query) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 1000 } 1001 1002 /** 1003 * Check if a user has creation rights on a query container 1004 * @param userIdentity the user identity 1005 * @param queryContainer the query container 1006 * @return true if the user has creation rights on a query container 1007 */ 1008 public boolean canCreate(UserIdentity userIdentity, QueryContainer queryContainer) 1009 { 1010 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, "/cms") == RightResult.RIGHT_ALLOW; 1011 } 1012 1013 1014 /** 1015 * Check if a user has write access on a query container 1016 * @param userIdentity the user identity 1017 * @param queryContainer the query container 1018 * @return true if the user has write access on the a query container 1019 */ 1020 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer) 1021 { 1022 return canWrite(userIdentity, queryContainer, false); 1023 } 1024 1025 /** 1026 * Check if a user has write access on a query container 1027 * @param userIdentity the user user identity 1028 * @param queryContainer the query container 1029 * @param recursively true to check write access on all descendants recursively 1030 * @return true if the user has write access on the a query container 1031 */ 1032 public boolean canWrite(UserIdentity userIdentity, QueryContainer queryContainer, boolean recursively) 1033 { 1034 boolean hasRight = _rightManager.hasRight(userIdentity, QUERY_CONTAINER_HANDLE_RIGHT_ID, queryContainer) == RightResult.RIGHT_ALLOW; 1035 if (!hasRight) 1036 { 1037 return false; 1038 } 1039 1040 if (recursively) 1041 { 1042 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1043 { 1044 for (AmetysObject child : children) 1045 { 1046 if (child instanceof QueryContainer) 1047 { 1048 hasRight = hasRight && canWrite(userIdentity, (QueryContainer) child, true); 1049 } 1050 else if (child instanceof Query) 1051 { 1052 hasRight = hasRight && canWrite(userIdentity, (Query) child); 1053 } 1054 1055 if (!hasRight) 1056 { 1057 return false; 1058 } 1059 } 1060 } 1061 } 1062 1063 return hasRight; 1064 } 1065 1066 /** 1067 * Check if a user can edit rights on a query container 1068 * @param userIdentity the user 1069 * @param queryContainer the query container 1070 * @return true if the user can edit rights on a query 1071 */ 1072 public boolean canAssignRights(UserIdentity userIdentity, QueryContainer queryContainer) 1073 { 1074 return canWrite(userIdentity, queryContainer) || _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW; 1075 } 1076 1077 /** 1078 * Gets all query containers for given parent 1079 * @param parent The {@link QueryContainer}, defining the context from which getting children 1080 * @return all query containers for given parent 1081 */ 1082 public AmetysObjectIterable<QueryContainer> getChildQueryContainers(QueryContainer parent) 1083 { 1084 return _resolver.query(QueryHelper.getXPathForQueryContainers(parent)); 1085 } 1086 1087 /** 1088 * Check if a folder have a descendant in read access for a given user 1089 * @param userIdentity the user 1090 * @param queryContainer the query container 1091 * @return true if the user have read right for at least one child of this container 1092 */ 1093 public boolean hasAnyReadableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1094 { 1095 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1096 { 1097 for (AmetysObject child : children) 1098 { 1099 if (child instanceof QueryContainer) 1100 { 1101 if (canRead(userIdentity, (QueryContainer) child) || hasAnyReadableDescendant(userIdentity, (QueryContainer) child)) 1102 { 1103 return true; 1104 } 1105 } 1106 else if (child instanceof Query && canRead(userIdentity, (Query) child)) 1107 { 1108 return true; 1109 } 1110 } 1111 } 1112 1113 return false; 1114 } 1115 1116 /** 1117 * Check if a query container have descendant in write access for a given user 1118 * @param userIdentity the user identity 1119 * @param queryContainer the query container 1120 * @return true if the user have write right for at least one child of this container 1121 */ 1122 public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1123 { 1124 boolean canWrite = false; 1125 1126 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1127 { 1128 for (AmetysObject child : children) 1129 { 1130 if (child instanceof QueryContainer) 1131 { 1132 if (canWrite(userIdentity, (QueryContainer) child) || hasAnyWritableDescendant(userIdentity, (QueryContainer) child)) 1133 { 1134 return true; 1135 } 1136 } 1137 else if (child instanceof Query && canWrite(userIdentity, (Query) child)) 1138 { 1139 return true; 1140 } 1141 } 1142 } 1143 1144 return canWrite; 1145 } 1146 1147 /** 1148 * Check if a folder have descendant in right assignment access for a given user 1149 * @param userIdentity the user identity 1150 * @param queryContainer the query container 1151 * @return true if the user have right assignment right for at least one child of this container 1152 */ 1153 public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, QueryContainer queryContainer) 1154 { 1155 try (AmetysObjectIterable<AmetysObject> children = queryContainer.getChildren()) 1156 { 1157 for (AmetysObject child : children) 1158 { 1159 if (child instanceof QueryContainer) 1160 { 1161 if (canAssignRights(userIdentity, (QueryContainer) child) || hasAnyAssignableDescendant(userIdentity, (QueryContainer) child)) 1162 { 1163 return true; 1164 } 1165 } 1166 else if (child instanceof Query) 1167 { 1168 if (canAssignRights(userIdentity, (Query) child)) 1169 { 1170 return true; 1171 } 1172 } 1173 } 1174 return false; 1175 } 1176 } 1177}