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