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 * 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 052 * @param currentAccessor the current accessor 053 * @param accessorToInclude the accessor to include 054 * @param referenceView the reference view 055 * @param accessorPath the path of the accessor in the reference view 056 */ 057 public static void addViewAccessorItems(ViewItemAccessor currentAccessor, ViewItemAccessor accessorToInclude, View referenceView, String accessorPath) 058 { 059 for (ViewItem itemToInclude : accessorToInclude.getViewItems()) 060 { 061 String itemPath = accessorPath; 062 if (itemToInclude instanceof ModelViewItem) 063 { 064 itemPath += StringUtils.isNotEmpty(accessorPath) ? ModelItem.ITEM_PATH_SEPARATOR + itemToInclude.getName() : itemToInclude.getName(); 065 } 066 067 if (!(itemToInclude instanceof ModelViewItem) || !referenceView.hasModelViewItem((ModelViewItem) itemToInclude, StringUtils.EMPTY, itemPath)) 068 { 069 ViewItem copy = itemToInclude.createInstance(); 070 currentAccessor.addViewItem(copy); 071 itemToInclude.copyTo(copy, referenceView, itemPath); 072 } 073 } 074 } 075 076 /** 077 * Creates a {@link ViewItemAccessor} with the items of the given {@link ModelItemAccessor} 078 * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor}) 079 * @param modelItemAccessors the model item accessors 080 * @return the created {@link ViewItemAccessor} 081 * @throws IllegalArgumentException if the model item accessors collection is empty 082 */ 083 public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException 084 { 085 T viewItemAccessor = createEmptyViewItemAccessor(modelItemAccessors); 086 087 for (ModelItemAccessor modelItemAccessor : modelItemAccessors) 088 { 089 for (ModelItem modelItem : modelItemAccessor.getModelItems()) 090 { 091 if (!viewItemAccessor.hasModelViewItem(modelItem.getName())) 092 { 093 addViewItem(modelItem.getName(), viewItemAccessor, modelItemAccessor); 094 } 095 } 096 } 097 098 return viewItemAccessor; 099 } 100 101 /** 102 * Creates a {@link ViewItemAccessor} with the given items 103 * @param <T> Type of the {@link ViewItemAccessor} to create ({@link View}, {@link ModelViewItemGroup} or {@link ViewElementAccessor}) 104 * @param modelItemAccessors the model items accessing the items definitions 105 * @param itemPaths the paths of the items to put in the view item accessor 106 * @return the created {@link ViewItemAccessor} 107 * @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 108 * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item 109 */ 110 public static <T extends ViewItemAccessor> T createViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors, String... itemPaths) throws IllegalArgumentException, BadItemTypeException 111 { 112 T viewItemContainer = createEmptyViewItemAccessor(modelItemAccessors); 113 114 for (String itemPath : itemPaths) 115 { 116 if (ModelHelper.hasModelItem(itemPath, modelItemAccessors)) 117 { 118 addViewItem(itemPath, viewItemContainer, modelItemAccessors.toArray(new ModelItemAccessor[modelItemAccessors.size()])); 119 } 120 else 121 { 122 String modelIds = StringUtils.join(modelItemAccessors.stream() 123 .map(modelItemContainer -> _getModelItemAccessorIdentifier(modelItemContainer)) 124 .collect(Collectors.toList()), ", "); 125 throw new IllegalArgumentException("Item '" + itemPath + "' not found in models: '" + modelIds + "'."); 126 } 127 } 128 129 return viewItemContainer; 130 } 131 132 /** 133 * Creates an empty {@link ViewItemAccessor} 134 * The created container can be a {@link View}, a {@link ModelViewItemGroup} or a {@link ViewElementAccessor}, according to the given {@link ModelItemAccessor}s. 135 * @param <T> The type of the created accessor 136 * @param modelItemAccessors the model items accessing the items definitions 137 * @return the created {@link ViewItemAccessor} 138 * @throws IllegalArgumentException if the model item accessors collection is empty 139 */ 140 @SuppressWarnings("unchecked") 141 public static <T extends ViewItemAccessor> T createEmptyViewItemAccessor(Collection<? extends ModelItemAccessor> modelItemAccessors) throws IllegalArgumentException 142 { 143 if (modelItemAccessors.isEmpty()) 144 { 145 throw new IllegalArgumentException("The model is needed to create a view item container"); 146 } 147 else 148 { 149 ModelItemAccessor firstModelItemAccessor = modelItemAccessors.iterator().next(); 150 if (firstModelItemAccessor instanceof ElementDefinition) 151 { 152 ViewElementAccessor viewItemAccesor = new ViewElementAccessor(); 153 viewItemAccesor.setDefinition((ElementDefinition) firstModelItemAccessor); 154 return (T) viewItemAccesor; 155 } 156 else if (firstModelItemAccessor instanceof ModelItemGroup) 157 { 158 ModelViewItemGroup viewItemAccessor = new ModelViewItemGroup(); 159 viewItemAccessor.setDefinition((ModelItemGroup) firstModelItemAccessor); 160 return (T) viewItemAccessor; 161 } 162 else 163 { 164 return (T) new View(); 165 } 166 } 167 } 168 169 /** 170 * Add a view item in the given accessor 171 * @param relativePath path of the item to add 172 * @param viewItemAccessor the view item accessor 173 * @param modelItemAccessors the corresponding model item accessors 174 * @throws IllegalArgumentException if the path is <code>null</code> or empty 175 * @throws UndefinedItemPathException if the path is not defined in the given model item accessors 176 * @throws BadItemTypeException if a segment in a path (but not the last) does not represent a group item 177 */ 178 @SuppressWarnings("unchecked") 179 public static void addViewItem(String relativePath, ViewItemAccessor viewItemAccessor, ModelItemAccessor... modelItemAccessors) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException 180 { 181 int firstIndexOfItemPathSeparator = relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR); 182 String firstPathSegment = firstIndexOfItemPathSeparator > -1 ? relativePath.substring(0, relativePath.indexOf(ModelItem.ITEM_PATH_SEPARATOR)) : relativePath; 183 184 ModelItem modelItem = ModelHelper.getModelItem(firstPathSegment, Arrays.asList(modelItemAccessors)); 185 186 // Create the view item and add it to the current view item container 187 ModelViewItem viewItem = null; 188 if (modelItem instanceof ModelItemAccessor) 189 { 190 if (viewItemAccessor.hasModelViewItem(firstPathSegment)) 191 { 192 viewItem = viewItemAccessor.getModelViewItem(firstPathSegment); 193 } 194 else 195 { 196 if (modelItem instanceof ModelItemGroup) 197 { 198 viewItem = new ModelViewItemGroup(); 199 } 200 else 201 { 202 viewItem = new ViewElementAccessor(); 203 } 204 viewItem.setDefinition(modelItem); 205 viewItemAccessor.addViewItem(viewItem); 206 } 207 } 208 else 209 { 210 viewItem = new ViewElement(); 211 viewItem.setDefinition(modelItem); 212 viewItemAccessor.addViewItem(viewItem); 213 } 214 215 if (firstIndexOfItemPathSeparator > -1) 216 { 217 if (modelItem instanceof ModelItemAccessor) 218 { 219 // Only the first segment of the path has been processed, now recursively process the next ones 220 String subPath = relativePath.substring(firstIndexOfItemPathSeparator + 1); 221 addViewItem(subPath, (ViewItemAccessor) viewItem, (ModelItemAccessor) modelItem); 222 } 223 else 224 { 225 // The path has several segments but the first one is not a view item accessor 226 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"); 227 } 228 } 229 else if (modelItem instanceof ModelItemContainer) 230 { 231 // The last segment is a container, add all its children in the current view item container 232 for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems()) 233 { 234 String newRelativePath = StringUtils.removeStart(child.getPath(), modelItem.getPath() + ModelItem.ITEM_PATH_SEPARATOR); 235 addViewItem(newRelativePath, (ViewItemAccessor) viewItem, (ModelItemContainer) modelItem); 236 } 237 } 238 } 239 240 private static Optional<String> _getModelItemAccessorIdentifier(ModelItemAccessor accessor) 241 { 242 if (accessor instanceof Model) 243 { 244 return Optional.ofNullable(((Model) accessor).getId()); 245 } 246 else if (accessor instanceof ModelItem) 247 { 248 return Optional.ofNullable(((ModelItem) accessor).getName()); 249 } 250 else 251 { 252 return Optional.empty(); 253 } 254 } 255 256 /** 257 * Retrieves the paths of all model items in the given {@link View} 258 * @param view the {@link View} 259 * @return the paths of all items 260 */ 261 public static Set<String> getModelItemsPathsFromView(View view) 262 { 263 return _getModelItemsFromViewItemContainer(view).keySet(); 264 } 265 266 /** 267 * Retrieves all model items in the given {@link View} 268 * @param view the {@link View} 269 * @return all model items 270 */ 271 public static Collection<ModelItem> getModelItemsFromView(View view) 272 { 273 return _getModelItemsFromViewItemContainer(view).values(); 274 } 275 276 /** 277 * Retrieves all model items in the given {@link ViewItemContainer}, indexed by their paths 278 * @param viewItemContainer the {@link ViewItemContainer} 279 * @return all model items 280 */ 281 private static Map<String, ModelItem> _getModelItemsFromViewItemContainer(ViewItemContainer viewItemContainer) 282 { 283 Map<String, ModelItem> result = new HashMap<>(); 284 285 for (ViewItem viewItem : viewItemContainer.getViewItems()) 286 { 287 if (viewItem instanceof ModelViewItem) 288 { 289 ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition(); 290 result.put(modelItem.getPath(), modelItem); 291 } 292 293 if (viewItem instanceof ViewItemContainer) 294 { 295 result.putAll(_getModelItemsFromViewItemContainer((ViewItemContainer) viewItem)); 296 } 297 } 298 299 return result; 300 } 301 302 /** 303 * Checks if all the items of the given view are present only once in the view 304 * Children of accessors that are not containers are not taken into account 305 * @param view the view to check 306 * @return <code>true</code> if all items are presents only once, <code>false</code> otherwise 307 */ 308 public static boolean areItemsPresentsOnlyOnce(View view) 309 { 310 return _areItemsPresentsOnlyOnce(view, new HashSet<>()); 311 } 312 313 private static boolean _areItemsPresentsOnlyOnce(ViewItemContainer containerToCheck, Set<String> paths) 314 { 315 for (ViewItem viewItem : containerToCheck.getViewItems()) 316 { 317 if (viewItem instanceof ModelViewItem) 318 { 319 String viewItemPath = ((ModelViewItem) viewItem).getDefinition().getPath(); 320 321 if (paths.contains(viewItemPath)) 322 { 323 return false; 324 } 325 else 326 { 327 paths.add(viewItemPath); 328 } 329 } 330 331 if (viewItem instanceof ViewItemContainer && !_areItemsPresentsOnlyOnce((ViewItemContainer) viewItem, paths)) 332 { 333 return false; 334 } 335 } 336 337 return true; 338 } 339 340 /** 341 * Retrieves a View corresponding to the given one, avoiding the view items below the {@link ViewItemAccessor}s that are not {@link ViewItemContainer}s 342 * @param originalView the view to truncate 343 * @return the truncated view 344 */ 345 public static View getTruncatedView(View originalView) 346 { 347 View view = new View(); 348 view.addViewItems(_copyItems(originalView)); 349 return view; 350 } 351 352 private static List<ViewItem> _copyItems(ViewItemContainer currentOrigContainer) 353 { 354 List<ViewItem> copies = new ArrayList<>(); 355 for (ViewItem origChild : currentOrigContainer.getViewItems()) 356 { 357 ViewItem destChild = origChild.createInstance(); 358 origChild.copyTo(destChild); 359 360 if (origChild instanceof ViewItemContainer) 361 { 362 ((ViewItemContainer) destChild).addViewItems(_copyItems((ViewItemContainer) origChild)); 363 } 364 365 copies.add(destChild); 366 } 367 368 return copies; 369 } 370 371 /** 372 * Retrieves a View corresponding to the given one, where items that appears several times are merged 373 * @param originalView the view to merge 374 * @return the merged view 375 */ 376 public static View mergeDuplicatedItems(View originalView) 377 { 378 View view = new View(); 379 380 _mergeDuplicatedItems(originalView, view, view, StringUtils.EMPTY); 381 382 return view; 383 } 384 385 private static void _mergeDuplicatedItems(ViewItemAccessor currentOrigAccessor, ViewItemAccessor currentDestAccessor, View destinationView, String accessorPath) 386 { 387 for (ViewItem origChild : currentOrigAccessor.getViewItems()) 388 { 389 if (origChild instanceof ModelViewItem) 390 { 391 String itemPath = StringUtils.isNotEmpty(accessorPath) ? accessorPath + ModelItem.ITEM_PATH_SEPARATOR + origChild.getName() : origChild.getName(); 392 if (ViewHelper.hasModelViewItem(destinationView, itemPath)) 393 { 394 // Ignore View elements, check view item accessors 395 if (origChild instanceof ViewItemAccessor) 396 { 397 ViewItem destChild = ViewHelper.getModelViewItem(destinationView, itemPath); 398 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, itemPath); 399 } 400 } 401 else 402 { 403 ViewItem destChild = origChild.createInstance(); 404 currentDestAccessor.addViewItem(destChild); 405 origChild.copyTo(destChild, destinationView, itemPath); 406 } 407 } 408 else 409 { 410 ViewItem destChild = origChild.createInstance(); 411 currentDestAccessor.addViewItem(destChild); 412 origChild.copyTo(destChild); 413 _mergeDuplicatedItems((ViewItemAccessor) origChild, (ViewItemAccessor) destChild, destinationView, accessorPath); 414 } 415 } 416 } 417 418 /** 419 * Checks if there is a {@link ModelViewItem} in the {@link ViewItemAccessor} at the given path 420 * @param viewItemAccessor The accessor of the view items 421 * @param itemPath The path of the item to check 422 * @return <code>true</code> if there is a model view item at the given path, <code>false</code> otherwise 423 */ 424 public static boolean hasModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) 425 { 426 try 427 { 428 getModelViewItem(viewItemAccessor, itemPath); 429 // The model item can never be null. If no excpetion has been thrown, the there is a model item at this path 430 return true; 431 } 432 catch (UndefinedItemPathException | BadItemTypeException e) 433 { 434 return false; 435 } 436 } 437 438 /** 439 * Converts the given view items as a JSON map 440 * @param viewItems the view items to convert 441 * @param context the context of the items' definitions 442 * @return The view items as a JSON map 443 * @throws ProcessingException If an error occurs when converting the view items 444 */ 445 public static Map<String, Object> viewItemsToJSON(List<ViewItem> viewItems, DefinitionContext context) throws ProcessingException 446 { 447 Map<String, Object> elements = new LinkedHashMap<>(); 448 449 for (ViewItem item : viewItems) 450 { 451 Map<String, Object> itemAsJSON = item.toJSON(context); 452 453 if (!itemAsJSON.isEmpty()) 454 { 455 String itemUUID = item.getName(); 456 if (StringUtils.isEmpty(itemUUID)) 457 { 458 itemUUID = UUID.randomUUID().toString(); 459 } 460 461 elements.put(itemUUID, itemAsJSON); 462 } 463 } 464 465 return elements; 466 } 467 468 /** 469 * Gets the {@link ModelViewItem} from the {@link ViewItemAccessor} at the given path. 470 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 471 * @param viewItemAccessor The accessor of view items 472 * @param itemPath The path of the item to get 473 * @return The {@link ModelViewItem}. Can never be <code>null</code> 474 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 475 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 476 */ 477 public static ModelViewItem getModelViewItem(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 478 { 479 return new ViewItemGetter(viewItemAccessor).getViewItem(itemPath); 480 } 481 482 /** 483 * Gets the {@link ViewElement} from the {@link ViewItemAccessor} at the given path. 484 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 485 * @param viewItemAccessor The accessor of view items 486 * @param itemPath The path of the item to get 487 * @return The {@link ViewElement}. Can never be <code>null</code> 488 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 489 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 490 */ 491 public static ViewElement getViewElement(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 492 { 493 return new ViewItemGetter(viewItemAccessor).getModelViewItem(itemPath); 494 } 495 496 /** 497 * Gets the {@link ModelViewItemGroup} from the {@link ViewItemAccessor} at the given path. 498 * <br>Unlike {@link View#getModelViewItem(String)}, this method accepts a path and not only a name, allowing to traverse composites. 499 * @param viewItemAccessor The accessor of view items 500 * @param itemPath The path of the container to get 501 * @return The {@link ModelViewItemGroup}. Can never be <code>null</code> 502 * @throws UndefinedItemPathException If one of the parts of the given path is undefined 503 * @throws BadItemTypeException If one of the parts of the given path is defined, but is not of the correct type 504 */ 505 public static ModelViewItemGroup getModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemPath) throws UndefinedItemPathException, BadItemTypeException 506 { 507 return new ViewItemGetter(viewItemAccessor).getViewItemContainer(itemPath); 508 } 509 510 private static class ViewItemGetter 511 { 512 private final ViewItemAccessor _viewItemAccessor; 513 private String _wholePath; 514 515 ViewItemGetter(ViewItemAccessor viewItemAccessor) 516 { 517 _viewItemAccessor = viewItemAccessor; 518 } 519 520 ModelViewItem getViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 521 { 522 _wholePath = wholePath; 523 BiFunction<ViewItemAccessor, String, ModelViewItem> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ModelViewItem.class); 524 return _getModelViewItem(lastPartGetter); 525 } 526 527 ViewElement getModelViewItem(String wholePath) throws UndefinedItemPathException, BadItemTypeException 528 { 529 _wholePath = wholePath; 530 BiFunction<ViewItemAccessor, String, ViewElement> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectViewItem(viewItemAccessor, lastPart, ViewElement.class); 531 return _getModelViewItem(lastPartGetter); 532 } 533 534 ModelViewItemGroup getViewItemContainer(String wholePath) throws UndefinedItemPathException, BadItemTypeException 535 { 536 _wholePath = wholePath; 537 BiFunction<ViewItemAccessor, String, ModelViewItemGroup> lastPartGetter = (viewItemAccessor, lastPart) -> _getDirectModelViewItemGroup(viewItemAccessor, lastPart); 538 return _getModelViewItem(lastPartGetter); 539 } 540 541 private <T> T _getModelViewItem(BiFunction<ViewItemAccessor, String, T> lastPartGetter) throws UndefinedItemPathException, BadItemTypeException 542 { 543 Deque<String> parts = new ArrayDeque<>(Arrays.asList(_wholePath.split(ModelItem.ITEM_PATH_SEPARATOR))); 544 String lastPart = parts.removeLast(); 545 ViewItemAccessor currentViewItemAccessor = _viewItemAccessor; 546 while (!parts.isEmpty()) 547 { 548 String currentPart = parts.pop(); 549 currentViewItemAccessor = _getDirectViewItem(currentViewItemAccessor, currentPart, ViewItemAccessor.class); 550 } 551 552 return lastPartGetter.apply(currentViewItemAccessor, lastPart); 553 } 554 555 private ModelViewItemGroup _getDirectModelViewItemGroup(ViewItemAccessor viewItemAccessor, String itemName) throws UndefinedItemPathException, BadItemTypeException 556 { 557 return _getDirectViewItem(viewItemAccessor, itemName, ModelViewItemGroup.class); 558 } 559 560 private <T> T _getDirectViewItem(ViewItemAccessor viewItemAccessor, String itemName, Class<T> resultClass) throws UndefinedItemPathException, BadItemTypeException 561 { 562 if (!viewItemAccessor.hasModelViewItem(itemName)) 563 { 564 throw new UndefinedItemPathException("For path '" + _wholePath + "', the part '" + itemName + "' is not defined"); 565 } 566 else 567 { 568 ModelViewItem modelViewItem = viewItemAccessor.getModelViewItem(itemName); 569 if (resultClass.isInstance(modelViewItem)) 570 { 571 return resultClass.cast(modelViewItem); 572 } 573 else 574 { 575 throw new BadItemTypeException("For path '" + _wholePath + "', the part '" + itemName + "' does not point to a '" + resultClass + "' (got a '" + modelViewItem.getClass().getName() + "')"); 576 } 577 } 578 } 579 } 580}