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 = Optional.ofNullable(viewItemAccessor.getViewItem(pathSegments[0])) 351 .filter(ViewItemAccessor.class::isInstance) 352 .map(ViewItemAccessor.class::cast) 353 .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) + "'")); 354 String subviewItemPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length); 355 return getViewItem(newViewItemAccessor, subviewItemPath); 356 } 357 } 358 359 /** 360 * Insert the given {@link ViewItem} to the view after the item at the given path 361 * @param viewItemAccessor The ViewItemAccessor in which to insert the view item 362 * @param viewItemToInsert The view item to insert 363 * @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 364 * @throws IllegalArgumentException If the given view item path is null or empty 365 * @throws UndefinedItemPathException If the given view item path is not present in the view item accessor 366 */ 367 public static void insertViewItemAfter(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertAfter) throws IllegalArgumentException, UndefinedItemPathException 368 { 369 insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertAfter, InsertMode.AFTER); 370 } 371 372 /** 373 * Insert the given {@link ViewItem} to the view before the item at the given path 374 * @param viewItemAccessor The ViewItemAccessor in which to insert the item 375 * @param viewItemToInsert The view item to insert 376 * @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 377 * @throws IllegalArgumentException If the ModelItem "insertBefore" is null or empty 378 * @throws UndefinedItemPathException If the path to the modelItem "insertBefore" is undefined 379 */ 380 public static void insertItemBefore(ViewItemAccessor viewItemAccessor, ViewItem viewItemToInsert, String insertBefore) throws IllegalArgumentException, UndefinedItemPathException 381 { 382 insertItemAfterOrBefore(viewItemAccessor, viewItemToInsert, insertBefore, InsertMode.BEFORE); 383 } 384 385 /** 386 * Insert the given {@link ViewItem} to the view before or after the item at the given path 387 * @param viewItemAccessor The ViewItemAccessor in which to insert the item 388 * @param itemToInsert The view item to insert 389 * @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 390 * @param insertMode The mode of insertion (before or after) 391 * @throws IllegalArgumentException If the ModelItem "insertAfterOrBefore" is null, empty, or is a path 392 * @throws UndefinedItemPathException If the path to the modelItem "insertAfterOrBefore" is undefined 393 */ 394 public static void insertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) throws IllegalArgumentException, UndefinedItemPathException 395 { 396 // Checks that the given item path is not empty 397 if (StringUtils.isBlank(insertAfterOrBefore)) 398 { 399 throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the given name. This name is empty."); 400 } 401 402 // Checks that the given item path is not a path 403 if (insertAfterOrBefore.contains(ModelItem.ITEM_PATH_SEPARATOR)) 404 { 405 throw new IllegalArgumentException("Unable to insert view item " + itemToInsert.getName() + " " + insertMode.toString() + " the view item with the name '" + insertAfterOrBefore + "'. This name is a path."); 406 } 407 408 if (!_doInsertItemAfterOrBefore(viewItemAccessor, itemToInsert, insertAfterOrBefore, insertMode)) 409 { 410 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) + "'"); 411 } 412 } 413 414 private static boolean _doInsertItemAfterOrBefore(ViewItemAccessor viewItemAccessor, ViewItem itemToInsert, String insertAfterOrBefore, InsertMode insertMode) 415 { 416 List<ViewItem> viewItems = viewItemAccessor.getViewItems(); 417 for (int indexInAccessor = 0; indexInAccessor < viewItems.size(); indexInAccessor++) 418 { 419 ViewItem viewItem = viewItems.get(indexInAccessor); 420 if (insertAfterOrBefore.equals(viewItem.getName())) 421 { 422 // If the viewItemAccessor is a ModelViewItem, then check if the itemToInsert is a part of the viewItemAccessor's model 423 if (viewItemAccessor instanceof ModelViewItem modelViewItem && !((ModelItemAccessor) modelViewItem.getDefinition()).hasModelItem(itemToInsert.getName())) 424 { 425 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."); 426 } 427 428 // Insert the view item to insert, in the view 429 viewItemAccessor.insertViewItem(itemToInsert, indexInAccessor + insertMode.getPositionFromReferencedViewItem()); 430 return true; 431 } 432 } 433 434 return false; 435 } 436 437 private static String _getViewItemAccessorName(ViewItemAccessor viewItemAccessor) 438 { 439 return viewItemAccessor instanceof View view 440 ? view.getName() 441 : viewItemAccessor instanceof ViewItem viewItem 442 ? viewItem.getName() 443 : viewItemAccessor.toString(); 444 } 445 446 /** 447 * Retrieves the paths of all model items in the given {@link View} 448 * @param view the {@link View} 449 * @return the paths of all items 450 */ 451 public static Set<String> getModelItemsPathsFromView(View view) 452 { 453 return _getModelItemsFromViewItemContainer(view).keySet(); 454 } 455 456 /** 457 * Retrieves all model items in the given {@link View} 458 * @param view the {@link View} 459 * @return all model items 460 */ 461 public static Collection<ModelItem> getModelItemsFromView(View view) 462 { 463 return _getModelItemsFromViewItemContainer(view).values(); 464 } 465 466 /** 467 * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths 468 * @param viewItemContainer the {@link ViewItemContainer} 469 * @return all model items 470 */ 471 private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer) 472 { 473 Map<String, ModelItem> result = new HashMap<>(); 474 475 for (ViewItem viewItem : viewItemContainer.getViewItems()) 476 { 477 if (viewItem instanceof ModelViewItem) 478 { 479 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 480 result.put(modelItem.getPath(), modelItem); 481 } 482 483 if (viewItem instanceof ViewItemContainer) 484 { 485 result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem)); 486 } 487 } 488 489 return result; 490 } 491 492 /** 493 * Checks if all the items of the given view are present only once in the view 494 * Children of accessors that are not containers are not taken into account 495 * @param view the view to check 496 * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise 497 */ 498 public static boolean areItemsPresentsOnlyOnce(View view) 499 { 500 return _areItemsPresentsOnlyOnce(view, new HashSet<>()); 501 } 502 503 private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths) 504 { 505 for (ViewItem viewItem : containerToCheck.getViewItems()) 506 { 507 if (viewItem instanceof ModelViewItem) 508 { 509 String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath(); 510 511 if (paths.contains(viewItemPath)) 512 { 513 return false; 514 } 515 else 516 { 517 paths.add(viewItemPath); 518 } 519 } 520 521 if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths)) 522 { 523 return false; 524 } 525 } 526 527 return true; 528 } 529 530 /** 531 * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s 532 * @param originalView the view to truncate 533 * @return the truncated view 534 */ 535 public static View getTruncatedView(View originalView) 536 { 537 View view = new View(); 538 view.addViewItems(_copyItemsForTruncatedView(originalView)); 539 return view; 540 } 541 542 private static List<ViewItem> _copyItemsForTruncatedView(ViewItemContainer currentOrigContainer) 543 { 544 List<ViewItem> copies = new ArrayList<>(); 545 for (ViewItem origChild : currentOrigContainer.getViewItems()) 546 { 547 // do not copy non editable item for the truncated view 548 if (_isEditable(origChild)) 549 { 550 ViewItem destChild = origChild.createInstance(); 551 origChild.copyTo(destChild); 552 553 // Get children of containers, not accessors for the truncated view 554 if (origChild instanceof ViewItemContainer) 555 { 556 ((ViewItemContainer) destChild).addViewItems(_copyItemsForTruncatedView((ViewItemContainer) origChild)); 557 } 558 559 copies.add(destChild); 560 } 561 } 562 563 return copies; 564 } 565 566 private static boolean _isEditable(ViewItem viewItem) 567 { 568 return viewItem instanceof ViewElement viewElement ? viewElement.getDefinition().isEditable() : true; 569 } 570 571 /** 572 * Retrieves a View corresponding to the given one, where items that appears several times are merged 573 * @param originalView the view to merge 574 * @return the merged view 575 */ 576 public static View mergeDuplicatedItems(View originalView) 577 { 578 View view = new View(); 579 580 _mergeDuplicatedItems(originalView, view, view, StringUtils.EMPTY); 581 582 return view; 583 } 584 585 private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, View destinationView, String accessorPath) 586 { 587 for (ViewItem origChild : currentOrigAccessor.getViewItems()) 588 { 589 if (origChild instanceof ModelViewItem) 590 { 591 String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName(); 592 if (ViewHelper.hasModelViewItem(destinationView, itemPath)) 593 { 594 // Ignore View elements, check view item accessors 595 if (origChild instanceof ViewItemAccessor) 596 { 597 ViewItem destChild = ViewHelper.getModelViewItem(destinationView, itemPath); 598 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath); 599 } 600 } 601 else 602 { 603 ViewItem destChild = origChild.createInstance(); 604 currentDestAccessor.addViewItem(destChild); 605 origChild.copyTo(destChild, destinationView, itemPath); 606 } 607 } 608 else 609 { 610 ViewItem destChild = origChild.createInstance(); 611 currentDestAccessor.addViewItem(destChild); 612 origChild.copyTo(destChild); 613 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, accessorPath); 614 } 615 } 616 } 617 618 /** 619 * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path 620 * @param viewItemAccessor The accessor of the view items 621 * @param itemPath The path of the item to check 622 * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise 623 */ 624 public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) 625 { 626 try 627 { 628 getModelViewItem(viewItemAccessor, itemPath); 629 // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path 630 return true; 631 } 632 catch (UndefinedItemPathException | BadItemTypeException e) 633 { 634 return false; 635 } 636 } 637 638 /** 639 * Converts the given view items as a JSON map 640 * @param viewItems the view items to convert 641 * @param context the context of the items' definitions 642 * @return The view items as a JSON map 643 * @throws ProcessingException If an error occurs when converting the view items 644 */ 645 public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context) throws ProcessingException 646 { 647 Map<String, Object> elements = new LinkedHashMap<>(); 648 649 for (ViewItem item : viewItems) 650 { 651 Map<String, Object> itemAsJSON = item.toJSON(context); 652 653 if (!itemAsJSON.isEmpty()) 654 { 655 String itemUUID = item.getName(); 656 if (StringUtils.isEmpty(itemUUID)) 657 { 658 itemUUID = UUID.randomUUID().toString(); 659 } 660 661 elements.put(itemUUID, itemAsJSON); 662 } 663 } 664 665 return elements; 666 } 667 668 /** 669 * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path. 670 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 671 * @param viewItemAccessor The accessor of view items 672 * @param itemPath The path of the item to get 673 * @return The {@link ModelViewItem}. Can never be <code>null</code> 674 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 675 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 676 */ 677 public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 678 { 679 return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath); 680 } 681 682 /** 683 * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path. 684 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 685 * @param viewItemAccessor The accessor of view items 686 * @param itemPath The path of the item to get 687 * @return The {@link ViewElement}. Can never be <code>null</code> 688 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 689 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 690 */ 691 public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 692 { 693 return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath); 694 } 695 696 /** 697 * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path. 698 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 699 * @param viewItemAccessor The accessor of view items 700 * @param itemPath The path of the container to get 701 * @return The {@link ModelViewItemGroup}. Can never be <code>null</code> 702 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 703 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 704 */ 705 public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 706 { 707 return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath); 708 } 709 710 private static class ViewItemGetter 711 { 712 private final ViewItemAccessor _viewItemAccessor; 713 private String _wholePath; 714 715 ViewItemGetter(ViewItemAccessor viewItemAccessor) 716 { 717 _viewItemAccessor = viewItemAccessor; 718 } 719 720 ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 721 { 722 _wholePath = wholePath; 723 BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class); 724 return _getModelViewItem(lastPartGetter); 725 } 726 727 ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 728 { 729 _wholePath = wholePath; 730 BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class); 731 return _getModelViewItem(lastPartGetter); 732 } 733 734 ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException 735 { 736 _wholePath = wholePath; 737 BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart); 738 return _getModelViewItem(lastPartGetter); 739 } 740 741 private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException 742 { 743 Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR))); 744 String lastPart = parts.removeLast(); 745 ViewItemAccessor currentViewItemAccessor = _viewItemAccessor; 746 while (!parts.isEmpty()) 747 { 748 String currentPart = parts.pop(); 749 currentViewItemAccessor = _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class); 750 } 751 752 return lastPartGetter.apply(currentViewItemAccessor, lastPart); 753 } 754 755 private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException 756 { 757 return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class); 758 } 759 760 private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException 761 { 762 if (!viewItemAccessor.hasModelViewItem(itemName)) 763 { 764 throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined"); 765 } 766 else 767 { 768 ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName); 769 if (resultClass.isInstance(modelViewItem)) 770 { 771 return resultClass.cast(modelViewItem); 772 } 773 else 774 { 775 throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')"); 776 } 777 } 778 } 779 } 780}