001/* 002 * Copyright 2018 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.runtime.model; 017 018import java.util.ArrayDeque; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Deque; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Optional; 029import java.util.Set; 030import java.util.UUID; 031import java.util.function.BiFunction; 032import java.util.stream.Collectors; 033 034import org.apache.cocoon.ProcessingException; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.runtime.model.exception.BadItemTypeException; 038import org.ametys.runtime.model.exception.UndefinedItemPathException; 039 040/** 041 * Helper class for views 042 */ 043public final class ViewHelper 044{ 045 private ViewHelper() 046 { 047 // Empty constructor 048 } 049 050 /** 051 * The mode used to insert a view item in an existing view item accessor 052 */ 053 public static enum InsertMode 054 { 055 /** Insert the view item before the referenced one */ 056 BEFORE (0), 057 /** Insert the view item after the referenced one */ 058 AFTER (1); 059 060 private final int _positionFromReferencedViewItem; 061 062 InsertMode(int offsetFromViewItem) 063 { 064 this._positionFromReferencedViewItem = offsetFromViewItem; 065 } 066 067 /** 068 * Retrieves the position of the item to insert compared to the referenced one 069 * @return the position of the item to insert compared to the referenced one 070 */ 071 public int getPositionFromReferencedViewItem() 072 { 073 return _positionFromReferencedViewItem; 074 } 075 076 @Override 077 public String toString() 078 { 079 return super.toString().toLowerCase(); 080 } 081 } 082 083 /** 084 * Retrieves the path of the given {@link ModelViewItem}. Only keep segments of the model view items, does not include groups 085 * @param modelViewItem the model view item 086 * @return the model view item's path 087 */ 088 public static String getModelViewItemPath(ModelViewItem modelViewItem) 089 { 090 String path = modelViewItem.getName(); 091 ViewItemAccessor parent = modelViewItem.getParent(); 092 while (parent != null) 093 { 094 if (parent instanceof ModelViewItem parentModelViewItem) 095 { 096 path = parentModelViewItem.getName() + ModelItem.ITEM_PATH_SEPARATOR + path; 097 } 098 099 parent = parent instanceof ViewItem parentViewItem ? parentViewItem.getParent() : null; 100 } 101 102 return path; 103 } 104 105 /** 106 * Copy the given view items. Also copy the children of view item accessors 107 * @param viewItems the view items to copy 108 * @return the view items copies 109 */ 110 public static List<ViewItem> copyViewItems(List<ViewItem> viewItems) 111 { 112 List<ViewItem> copies = new ArrayList<>(); 113 114 for (ViewItem viewItem : viewItems) 115 { 116 ViewItem copy = viewItem.createInstance(); 117 viewItem.copyTo(copy); 118 119 if (viewItem instanceof ViewItemAccessor viewItemAccessor) 120 { 121 List<ViewItem> copyChildren = copyViewItems(viewItemAccessor.getViewItems()); 122 ((ViewItemAccessor) copy).addViewItems(copyChildren); 123 } 124 125 copies.add(copy); 126 } 127 128 return copies; 129 } 130 131 /** 132 * Add the items of the view item accessor to include if they are not already present in the current view item accessor or in the reference view 133 * @param currentAccessor the current accessor 134 * @param accessorToInclude the accessor to include 135 * @param referenceView the reference view 136 * @param accessorPath the path of the accessor in the reference view 137 */ 138 public static void addViewAccessorItems(ViewItemAccessor currentAccessor, ViewItemAccessor accessorToInclude, View referenceView, String accessorPath) 139 { 140 for (ViewItem itemToInclude : accessorToInclude.getViewItems()) 141 { 142 String itemPath = accessorPath; 143 if (itemToInclude instanceof ModelViewItem) 144 { 145 itemPath += StringUtils.isNotEmpty(accessorPath) ? ModelItem.ITEM_PATH_SEPARATOR + itemToInclude.getName() : itemToInclude.getName(); 146 } 147 148 if (!(itemToInclude instanceof ModelViewItem) || !referenceView.hasModelViewItem((ModelViewItem) itemToInclude, StringUtils.EMPTY, itemPath)) 149 { 150 ViewItem copy = itemToInclude.createInstance(); 151 currentAccessor.addViewItem(copy); 152 itemToInclude.copyTo(copy, referenceView, itemPath); 153 } 154 } 155 } 156 157 /** 158 * Creates a {@link ViewItemAccessor} with the items of the given {@link ModelItemAccessor} 159 * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor}) 160 * @param modelItemAccessors the model item accessors 161 * @return the created {@link ViewItemAccessor} 162 * @throws IllegalArgumentException if the model item accessors collection is empty 163 */ 164 public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException 165 { 166 T viewItemAccessor = createEmptyViewItemAccessor(modelItemAccessors); 167 168 for (ModelItemAccessor modelItemAccessor : modelItemAccessors) 169 { 170 for (ModelItem modelItem : modelItemAccessor.getModelItems()) 171 { 172 if (!viewItemAccessor.hasModelViewItem(modelItem.getName())) 173 { 174 addViewItem(modelItem.getName(), viewItemAccessor, modelItemAccessor); 175 } 176 } 177 } 178 179 return viewItemAccessor; 180 } 181 182 /** 183 * Creates a {@link ViewItemAccessor} with the given items 184 * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor}) 185 * @param modelItemAccessors the model items accessing the items definitions 186 * @param itemPaths the paths of the items to put in the view item accessor 187 * @return the created {@link ViewItemAccessor} 188 * @throws IllegalArgumentException if the model item containers collection is empty or if an item path is <code>null</code>, empty, or is not defined in the given model item containers 189 * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item 190 */ 191 public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors, String... itemPaths) throws IllegalArgumentException, BadItemTypeException 192 { 193 T viewItemContainer = createEmptyViewItemAccessor(modelItemAccessors); 194 195 for (String itemPath : itemPaths) 196 { 197 if (ModelHelper.hasModelItem(itemPath, modelItemAccessors)) 198 { 199 addViewItem(itemPath, viewItemContainer, modelItemAccessors.toArray(new ModelItemAccessor[modelItemAccessors.size()])); 200 } 201 else 202 { 203 String modelIds = StringUtils.join(modelItemAccessors.stream() 204 .map(modelItemContainer -> _getModelItemAccessorIdentifier(modelItemContainer)) 205 .collect(Collectors.toList()), ", "); 206 throw new IllegalArgumentException("Item '" + itemPath + "' not found in models: '" + modelIds + "'."); 207 } 208 } 209 210 return viewItemContainer; 211 } 212 213 /** 214 * Creates an empty {@link ViewItemAccessor} 215 * The created container can be a {@link View}, a {@link ModelViewItemGroup} or a {@link ViewElementAccessor}, according to the given {@link ModelItemAccessor}s. 216 * @param <T> The type of the created accessor 217 * @param modelItemAccessors the model items accessing the items definitions 218 * @return the created {@link ViewItemAccessor} 219 * @throws IllegalArgumentException if the model item accessors collection is empty 220 */ 221 @SuppressWarnings("unchecked") 222 public static <T extends ViewItemAccessor> T createEmptyViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException 223 { 224 if (modelItemAccessors.isEmpty()) 225 { 226 throw new IllegalArgumentException("The model is needed to create a view item container"); 227 } 228 else 229 { 230 ModelItemAccessor firstModelItemAccessor = modelItemAccessors.iterator().next(); 231 if (firstModelItemAccessor instanceof ElementDefinition) 232 { 233 ViewElementAccessor viewItemAccesor = new ViewElementAccessor(); 234 viewItemAccesor.setDefinition((ElementDefinition) firstModelItemAccessor); 235 return (T) viewItemAccesor; 236 } 237 else if (firstModelItemAccessor instanceof ModelItemGroup) 238 { 239 ModelViewItemGroup viewItemAccessor = new ModelViewItemGroup(); 240 viewItemAccessor.setDefinition((ModelItemGroup) firstModelItemAccessor); 241 return (T) viewItemAccessor; 242 } 243 else 244 { 245 return (T) new View(); 246 } 247 } 248 } 249 250 /** 251 * Add a view item in the given accessor 252 * @param relativePath path of the item to add 253 * @param viewItemAccessor the view item accessor 254 * @param modelItemAccessors the corresponding model item accessors 255 * @return the added view item 256 * @throws IllegalArgumentException if the path is <code>null</code> or empty 257 * @throws UndefinedItemPathException if the path is not defined in the given model item accessors 258 * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item 259 */ 260 @SuppressWarnings("unchecked") 261 public static ViewItem addViewItem(String relativePath, ViewItemAccessor viewItemAccessor, ModelItemAccessor... modelItemAccessors) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException 262 { 263 int firstIndexOfItemPathSeparator = relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR); 264 String firstPathSegment = firstIndexOfItemPathSeparator > -1 ? relativePath.substring(0, relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR)) : relativePath; 265 266 ModelItem modelItem = ModelHelper.getModelItem(firstPathSegment, Arrays.asList(modelItemAccessors)); 267 268 // Create the view item and add it to the current view item container 269 ModelViewItem viewItem = null; 270 if (modelItem instanceof ModelItemAccessor) 271 { 272 if (viewItemAccessor.hasModelViewItem(firstPathSegment)) 273 { 274 viewItem = viewItemAccessor.getModelViewItem(firstPathSegment); 275 } 276 else 277 { 278 if (modelItem instanceof ModelItemGroup) 279 { 280 viewItem = new ModelViewItemGroup(); 281 } 282 else 283 { 284 viewItem = new ViewElementAccessor(); 285 } 286 viewItem.setDefinition(modelItem); 287 viewItemAccessor.addViewItem(viewItem); 288 } 289 } 290 else 291 { 292 viewItem = new ViewElement(); 293 viewItem.setDefinition(modelItem); 294 viewItemAccessor.addViewItem(viewItem); 295 } 296 297 if (firstIndexOfItemPathSeparator > -1) 298 { 299 if (modelItem instanceof ModelItemAccessor) 300 { 301 // Only the first segment of the path has been processed, now recursively process the next ones 302 String subPath = relativePath.substring(firstIndexOfItemPathSeparator + 1); 303 return addViewItem(subPath, (ViewItemAccessor) viewItem, (ModelItemAccessor) modelItem); 304 } 305 else 306 { 307 // The path has several segments but the first one is not a view item accessor 308 throw new BadItemTypeException("The segments inside the items' paths can only refer to an accessor. The path '" + relativePath + "' refers to the item '" + modelItem.getPath() + "' that is not an accessor"); 309 } 310 } 311 else if (modelItem instanceof ModelItemContainer) 312 { 313 // The last segment is a container, add all its children in the current view item container 314 for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems()) 315 { 316 String newRelativePath = StringUtils.removeStart(child.getPath(), modelItem.getPath() + ModelItem.ITEM_PATH_SEPARATOR); 317 addViewItem(newRelativePath, (ViewItemAccessor) viewItem, (ModelItemContainer) modelItem); 318 } 319 } 320 321 return viewItem; 322 } 323 324 private static Optional<String> _getModelItemAccessorIdentifier(ModelItemAccessor accessor) 325 { 326 if (accessor instanceof Model) 327 { 328 return Optional.ofNullable(((Model) accessor).getId()); 329 } 330 else if (accessor instanceof ModelItem) 331 { 332 return Optional.ofNullable(((ModelItem) accessor).getName()); 333 } 334 else 335 { 336 return Optional.empty(); 337 } 338 } 339 340 /** 341 * Retrieves the {@link SimpleViewItemGroup} at the given group path. All segments in the group path must refer to a {@link SimpleViewItemGroup} (tab, fieldset) 342 * If there are more than one corresponding items, the first one is retrieved 343 * @param viewItemAccessor The ViewItemAccessor in which to find the group of given path 344 * @param groupPath The group path 345 * @throws IllegalArgumentException if the group path is empty 346 * @throws UndefinedItemPathException if no group was found at the given path 347 * @return the found {@link SimpleViewItemGroup} 348 */ 349 public static SimpleViewItemGroup getSimpleViewItemGroup(ViewItemAccessor viewItemAccessor, String groupPath) 350 { 351 String[] pathSegments = groupPath.split(ModelItem.ITEM_PATH_SEPARATOR); 352 353 if (pathSegments == null || pathSegments.length < 1) 354 { 355 throw new IllegalArgumentException("Unable to retrieve the group at the given path. This path is empty."); 356 } 357 else if (pathSegments.length == 1) 358 { 359 String groupName = groupPath; 360 return Optional.ofNullable(viewItemAccessor.getViewItem(groupName)) 361 .filter(SimpleViewItemGroup.class::isInstance) 362 .map(SimpleViewItemGroup.class::cast) 363 .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the group with the name '" + groupName + "'. No group with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'")); 364 } 365 else 366 { 367 SimpleViewItemGroup simpleViewItemGroup = getSimpleViewItemGroup(viewItemAccessor, pathSegments[0]); 368 String subGroupPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 369 return getSimpleViewItemGroup(simpleViewItemGroup, subGroupPath); 370 } 371 } 372 373 /** 374 * Retrieves the {@link ViewItem} at the given view path. If items are in groups, the names of the groups must appear in the path. All groups have to be named 375 * If there are more than one corresponding items, the first one is retrieved 376 * @param viewItemAccessor The ViewItemAccessor in which to find the view item of given path 377 * @param viewItemPath The view item path 378 * @throws IllegalArgumentException if the view item path is empty 379 * @throws UndefinedItemPathException if no view item was found at the given path 380 * @return the found {@link ViewItem} 381 */ 382 public static ViewItem getViewItem(ViewItemAccessor viewItemAccessor, String viewItemPath) 383 { 384 String[] pathSegments = viewItemPath.split(ModelItem.ITEM_PATH_SEPARATOR); 385 386 if (pathSegments == null || pathSegments.length < 1) 387 { 388 throw new IllegalArgumentException("Unable to retrieve the view item at the given path. This path is empty."); 389 } 390 else if (pathSegments.length == 1) 391 { 392 String viewItemName = viewItemPath; 393 return Optional.ofNullable(viewItemAccessor.getViewItem(viewItemName)) 394 .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the view item with the name '" + viewItemName + "'. No view item with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'")); 395 } 396 else 397 { 398 ViewItemAccessor newViewItemAccessor = viewItemAccessor.getViewItems().stream() 399 .filter(viewItem -> pathSegments[0].equals(viewItem.getName())) 400 .filter(ViewItemAccessor.class::isInstance) 401 .map(ViewItemAccessor.class::cast) 402 .findFirst() 403 .orElseThrow(() -> new UndefinedItemPathException("Unable to retrieve the view item accessor with the name '" + pathSegments[0] + "'. No view item accessor with this name has been found in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'")); 404 String subviewItemPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 405 return getViewItem(newViewItemAccessor, subviewItemPath); 406 } 407 } 408 409 /** 410 * Insert the given {@link ViewItem} to the view after the item at the given path 411 * @param viewItemAccessor The ViewItemAccessor in which to insert the view item 412 * @param viewItemToInsert The view item to insert 413 * @param insertAfter The name of the view item after which the given one has to be inserted. The item with this name must appear in the given accessor 414 * @throws IllegalArgumentException If the given view item path is null or empty 415 * @throws UndefinedItemPathException If the given view item path is not present in the view item accessor 416 */ 417 public static void insertViewItemAfter(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertAfter) throws IllegalArgumentException, UndefinedItemPathException 418 { 419 insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertAfter, InsertMode.AFTER); 420 } 421 422 /** 423 * Insert the given {@link ViewItem} to the view before the item at the given path 424 * @param viewItemAccessor The ViewItemAccessor in which to insert the item 425 * @param viewItemToInsert The view item to insert 426 * @param insertBefore The name of the view item before which the given one has to be inserted. The item with this name must appear in the given accessor 427 * @throws IllegalArgumentException If the ModelItem "insertBefore" is null or empty 428 * @throws UndefinedItemPathException If the path to the modelItem "insertBefore" is undefined 429 */ 430 public static void insertItemBefore(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertBefore) throws IllegalArgumentException, UndefinedItemPathException 431 { 432 insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertBefore, InsertMode.BEFORE); 433 } 434 435 /** 436 * Insert the given {@link ViewItem} to the view before or after the item at the given path 437 * @param viewItemAccessor The ViewItemAccessor in which to insert the item 438 * @param itemToInsert The view item to insert 439 * @param insertAfterOrBefore The name of the view item before or after which the given one has to be inserted. The item with this name must appear in the given accessor 440 * @param insertMode The mode of insertion (before or after) 441 * @throws IllegalArgumentException If the ModelItem "insertAfterOrBefore" is null, empty, or is a path 442 * @throws UndefinedItemPathException If the path to the modelItem "insertAfterOrBefore" is undefined 443 */ 444 public static void insertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) throws IllegalArgumentException, UndefinedItemPathException 445 { 446 // Checks that the given item path is not empty 447 if (StringUtils.isBlank(insertAfterOrBefore)) 448 { 449 throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the given name. This name is empty."); 450 } 451 452 // Checks that the given item path is not a path 453 if (insertAfterOrBefore.contains(ModelItem.ITEM_PATH_SEPARATOR)) 454 { 455 throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the name '" + insertAfterOrBefore + "'. This name is a path."); 456 } 457 458 if (!_doInsertItemAfterOrBefore(viewItemAccessor, itemToInsert, insertAfterOrBefore, insertMode)) 459 { 460 throw new UndefinedItemPathException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item at path " + insertAfterOrBefore + ". No view item has been found at this path in view item accessor '" + _getViewItemAccessorName(viewItemAccessor) + "'"); 461 } 462 } 463 464 private static boolean _doInsertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) 465 { 466 List<ViewItem> viewItems = viewItemAccessor.getViewItems(); 467 for (int indexInAccessor = 0; indexInAccessor < viewItems.size(); indexInAccessor++) 468 { 469 ViewItem viewItem = viewItems.get(indexInAccessor); 470 if (insertAfterOrBefore.equals(viewItem.getName())) 471 { 472 // If the viewItemAccessor is a ModelViewItem, then check if the itemToInsert is a part of the viewItemAccessor's model 473 if (viewItemAccessor instanceof ModelViewItem modelViewItem && !((ModelItemAccessor) modelViewItem.getDefinition()).hasModelItem(itemToInsert.getName())) 474 { 475 throw new UndefinedItemPathException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item at path " + insertAfterOrBefore + ". The item to insert is not defined by the model."); 476 } 477 478 // Insert the view item to insert, in the view 479 viewItemAccessor.insertViewItem(itemToInsert, indexInAccessor + insertMode.getPositionFromReferencedViewItem()); 480 return true; 481 } 482 } 483 484 return false; 485 } 486 487 private static String _getViewItemAccessorName(ViewItemAccessor viewItemAccessor) 488 { 489 return viewItemAccessor instanceof View view 490 ? view.getName() 491 : viewItemAccessor instanceof ViewItem viewItem 492 ? viewItem.getName() 493 : viewItemAccessor.toString(); 494 } 495 496 /** 497 * Retrieves the paths of all model items in the given {@link View} 498 * @param view the {@link View} 499 * @return the paths of all items 500 */ 501 public static Set<String> getModelItemsPathsFromView(View view) 502 { 503 return _getModelItemsFromViewItemContainer(view).keySet(); 504 } 505 506 /** 507 * Retrieves all model items in the given {@link View} 508 * @param view the {@link View} 509 * @return all model items 510 */ 511 public static Collection<ModelItem> getModelItemsFromView(View view) 512 { 513 return _getModelItemsFromViewItemContainer(view).values(); 514 } 515 516 /** 517 * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths 518 * @param viewItemContainer the {@link ViewItemContainer} 519 * @return all model items 520 */ 521 private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer) 522 { 523 Map<String, ModelItem> result = new HashMap<>(); 524 525 for (ViewItem viewItem : viewItemContainer.getViewItems()) 526 { 527 if (viewItem instanceof ModelViewItem) 528 { 529 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 530 result.put(modelItem.getPath(), modelItem); 531 } 532 533 if (viewItem instanceof ViewItemContainer) 534 { 535 result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem)); 536 } 537 } 538 539 return result; 540 } 541 542 /** 543 * Checks if all the items of the given view are present only once in the view 544 * Children of accessors that are not containers are not taken into account 545 * @param view the view to check 546 * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise 547 */ 548 public static boolean areItemsPresentsOnlyOnce(View view) 549 { 550 return _areItemsPresentsOnlyOnce(view, new HashSet<>()); 551 } 552 553 private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths) 554 { 555 for (ViewItem viewItem : containerToCheck.getViewItems()) 556 { 557 if (viewItem instanceof ModelViewItem) 558 { 559 String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath(); 560 561 if (paths.contains(viewItemPath)) 562 { 563 return false; 564 } 565 else 566 { 567 paths.add(viewItemPath); 568 } 569 } 570 571 if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths)) 572 { 573 return false; 574 } 575 } 576 577 return true; 578 } 579 580 /** 581 * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s 582 * @param originalView the view to truncate 583 * @return the truncated view 584 */ 585 public static View getTruncatedView(View originalView) 586 { 587 View view = new View(); 588 view.addViewItems(_copyItemsForTruncatedView(originalView)); 589 return view; 590 } 591 592 private static List<ViewItem> _copyItemsForTruncatedView(ViewItemContainer currentOrigContainer) 593 { 594 List<ViewItem> copies = new ArrayList<>(); 595 for (ViewItem origChild : currentOrigContainer.getViewItems()) 596 { 597 // do not copy non editable item for the truncated view 598 if (_isEditable(origChild)) 599 { 600 ViewItem destChild = origChild.createInstance(); 601 origChild.copyTo(destChild); 602 603 // Get children of containers, not accessors for the truncated view 604 if (origChild instanceof ViewItemContainer) 605 { 606 ((ViewItemContainer) destChild).addViewItems(_copyItemsForTruncatedView((ViewItemContainer) origChild)); 607 } 608 609 copies.add(destChild); 610 } 611 } 612 613 return copies; 614 } 615 616 private static boolean _isEditable(ViewItem viewItem) 617 { 618 return viewItem instanceof ViewElement viewElement ? viewElement.getDefinition().isEditable() : true; 619 } 620 621 /** 622 * Retrieves a View corresponding to the given one, where items that appears several times are merged 623 * @param originalView the view to merge 624 * @return the merged view 625 */ 626 public static View mergeDuplicatedItems(View originalView) 627 { 628 View view = new View(); 629 630 _mergeDuplicatedItems(originalView, view, view, StringUtils.EMPTY); 631 632 return view; 633 } 634 635 private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, View destinationView, String accessorPath) 636 { 637 for (ViewItem origChild : currentOrigAccessor.getViewItems()) 638 { 639 if (origChild instanceof ModelViewItem) 640 { 641 String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName(); 642 if (ViewHelper.hasModelViewItem(destinationView, itemPath)) 643 { 644 // Ignore View elements, check view item accessors 645 if (origChild instanceof ViewItemAccessor) 646 { 647 ViewItem destChild = ViewHelper.getModelViewItem(destinationView, itemPath); 648 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath); 649 } 650 } 651 else 652 { 653 ViewItem destChild = origChild.createInstance(); 654 currentDestAccessor.addViewItem(destChild); 655 656 if (origChild instanceof ViewItemAccessor) 657 { 658 origChild.copyTo(destChild); 659 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath); 660 } 661 else 662 { 663 origChild.copyTo(destChild, destinationView, itemPath); 664 } 665 } 666 } 667 else 668 { 669 ViewItem destChild = origChild.createInstance(); 670 currentDestAccessor.addViewItem(destChild); 671 origChild.copyTo(destChild); 672 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, accessorPath); 673 } 674 } 675 } 676 677 /** 678 * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path 679 * @param viewItemAccessor The accessor of the view items 680 * @param itemPath The path of the item to check 681 * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise 682 */ 683 public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) 684 { 685 try 686 { 687 getModelViewItem(viewItemAccessor, itemPath); 688 // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path 689 return true; 690 } 691 catch (UndefinedItemPathException | BadItemTypeException e) 692 { 693 return false; 694 } 695 } 696 697 /** 698 * Converts the given view items as a JSON map 699 * @param viewItems the view items to convert 700 * @param context the context of the items' definitions 701 * @return The view items as a JSON map 702 * @throws ProcessingException If an error occurs when converting the view items 703 */ 704 public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context) throws ProcessingException 705 { 706 Map<String, Object> elements = new LinkedHashMap<>(); 707 708 for (ViewItem item : viewItems) 709 { 710 Map<String, Object> itemAsJSON = item.toJSON(context); 711 712 if (!itemAsJSON.isEmpty()) 713 { 714 String itemUUID = item.getName(); 715 if (StringUtils.isEmpty(itemUUID)) 716 { 717 itemUUID = UUID.randomUUID().toString(); 718 } 719 720 elements.put(itemUUID, itemAsJSON); 721 } 722 } 723 724 return elements; 725 } 726 727 /** 728 * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path. 729 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 730 * @param viewItemAccessor The accessor of view items 731 * @param itemPath The path of the item to get 732 * @return The {@link ModelViewItem}. Can never be <code>null</code> 733 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 734 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 735 */ 736 public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 737 { 738 return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath); 739 } 740 741 /** 742 * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path. 743 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 744 * @param viewItemAccessor The accessor of view items 745 * @param itemPath The path of the item to get 746 * @return The {@link ViewElement}. Can never be <code>null</code> 747 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 748 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 749 */ 750 public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 751 { 752 return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath); 753 } 754 755 /** 756 * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path. 757 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 758 * @param viewItemAccessor The accessor of view items 759 * @param itemPath The path of the container to get 760 * @return The {@link ModelViewItemGroup}. Can never be <code>null</code> 761 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 762 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 763 */ 764 public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 765 { 766 return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath); 767 } 768 769 private static class ViewItemGetter 770 { 771 private final ViewItemAccessor _viewItemAccessor; 772 private String _wholePath; 773 774 ViewItemGetter(ViewItemAccessor viewItemAccessor) 775 { 776 _viewItemAccessor = viewItemAccessor; 777 } 778 779 ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 780 { 781 _wholePath = wholePath; 782 BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class); 783 return _getModelViewItem(lastPartGetter); 784 } 785 786 ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 787 { 788 _wholePath = wholePath; 789 BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class); 790 return _getModelViewItem(lastPartGetter); 791 } 792 793 ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException 794 { 795 _wholePath = wholePath; 796 BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart); 797 return _getModelViewItem(lastPartGetter); 798 } 799 800 private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException 801 { 802 Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR))); 803 String lastPart = parts.removeLast(); 804 ViewItemAccessor currentViewItemAccessor = _viewItemAccessor; 805 while (!parts.isEmpty()) 806 { 807 String currentPart = parts.pop(); 808 currentViewItemAccessor = _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class); 809 } 810 811 return lastPartGetter.apply(currentViewItemAccessor, lastPart); 812 } 813 814 private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException 815 { 816 return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class); 817 } 818 819 private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException 820 { 821 if (!viewItemAccessor.hasModelViewItem(itemName)) 822 { 823 throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined"); 824 } 825 else 826 { 827 ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName); 828 if (resultClass.isInstance(modelViewItem)) 829 { 830 return resultClass.cast(modelViewItem); 831 } 832 else 833 { 834 throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')"); 835 } 836 } 837 } 838 } 839}