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