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