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