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.plugins.repository.data.holder.impl; 017 018import java.lang.reflect.Array; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028import java.util.stream.Stream; 029 030import org.apache.avalon.framework.activity.Disposable; 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.XMLUtils; 037import org.apache.commons.lang3.ArrayUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.tuple.ImmutablePair; 040import org.apache.commons.lang3.tuple.Pair; 041import org.xml.sax.ContentHandler; 042import org.xml.sax.SAXException; 043 044import org.ametys.core.ui.Callable; 045import org.ametys.core.util.DateUtils; 046import org.ametys.plugins.repository.AmetysObject; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.data.DataComment; 049import org.ametys.plugins.repository.data.UnknownDataException; 050import org.ametys.plugins.repository.data.ametysobject.ModelAwareDataAwareAmetysObject; 051import org.ametys.plugins.repository.data.ametysobject.ModelLessDataAwareAmetysObject; 052import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 053import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 054import org.ametys.plugins.repository.data.holder.DataHolder; 055import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 056import org.ametys.plugins.repository.data.holder.ModelLessDataHolder; 057import org.ametys.plugins.repository.data.holder.ModifiableDataHolder; 058import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 059import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 060import org.ametys.plugins.repository.data.holder.group.Composite; 061import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 062import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater; 063import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry; 064import org.ametys.plugins.repository.data.holder.group.ModifiableComposite; 065import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater; 066import org.ametys.plugins.repository.data.holder.group.Repeater; 067import org.ametys.plugins.repository.data.holder.group.RepeaterEntry; 068import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 069import org.ametys.plugins.repository.data.holder.values.SynchronizationContext; 070import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 071import org.ametys.plugins.repository.data.holder.values.ValueContext; 072import org.ametys.plugins.repository.model.RepeaterDefinition; 073import org.ametys.plugins.repository.model.ViewHelper; 074import org.ametys.runtime.model.ElementDefinition; 075import org.ametys.runtime.model.ModelItem; 076import org.ametys.runtime.model.ModelViewItem; 077import org.ametys.runtime.model.ViewItem; 078import org.ametys.runtime.model.ViewItemAccessor; 079import org.ametys.runtime.model.ViewItemContainer; 080import org.ametys.runtime.model.exception.BadDataPathCardinalityException; 081import org.ametys.runtime.model.exception.BadItemTypeException; 082import org.ametys.runtime.model.exception.NotUniqueTypeException; 083import org.ametys.runtime.model.exception.UndefinedItemPathException; 084import org.ametys.runtime.model.exception.UnknownTypeException; 085import org.ametys.runtime.model.type.DataContext; 086import org.ametys.runtime.model.type.ModelItemType; 087 088/** 089 * Helper for implementations of data holder 090 */ 091public final class DataHolderHelper implements Component, Serviceable, Disposable 092{ 093 /** Pattern for repeater entry : entryName[i] */ 094 public static final Pattern REPEATER_ENTRY_PATTERN = Pattern.compile("(.*)\\[(\\d+)\\]$"); 095 096 private static ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 097 private static AmetysObjectResolver _resolver; 098 099 @Override 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 103 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 104 } 105 106 public void dispose() 107 { 108 _externalizableDataProviderEP = null; 109 _resolver = null; 110 } 111 112 /** 113 * Checks if there is a repeater entry at the given position 114 * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater 115 * @param repeaterName the name of the repeater 116 * @param entryPosition the position of the entry 117 * @return <code>true</code> if there is an entry at the given position, <code>false</code> otherwise 118 * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater 119 */ 120 public static boolean hasRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException 121 { 122 if (dataHolder.getRepositoryData().hasValue(repeaterName)) 123 { 124 Repeater repeater = dataHolder.getRepeater(repeaterName); 125 return repeater.hasEntry(entryPosition); 126 } 127 128 return false; 129 } 130 131 /** 132 * Checks if there is a non empty repeater entry at the given position 133 * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater 134 * @param repeaterName the name of the repeater 135 * @param entryPosition the position of the entry 136 * @return <code>true</code> if there is a non empty entry at the given position, <code>false</code> otherwise 137 * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater 138 */ 139 public static boolean hasNonEmptyRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException 140 { 141 if (dataHolder.getRepositoryData().hasValue(repeaterName)) 142 { 143 Repeater repeater = dataHolder.getRepeater(repeaterName); 144 if (repeater.hasEntry(entryPosition)) 145 { 146 RepeaterEntry entry = repeater.getEntry(entryPosition); 147 return !entry.getDataNames().isEmpty(); 148 } 149 } 150 151 return false; 152 } 153 154 /** 155 * Retrieves the repeater entry at the given position 156 * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater 157 * @param repeaterName the name of the repeater 158 * @param entryPosition the position of the entry 159 * @return the repeater entry 160 * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater 161 */ 162 public static RepeaterEntry getRepeaterEntry(ModelAwareDataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException 163 { 164 Repeater repeater = dataHolder.getRepeater(repeaterName); 165 166 if (repeater == null) 167 { 168 return null; 169 } 170 171 if (repeater.hasEntry(entryPosition)) 172 { 173 return repeater.getEntry(entryPosition); 174 } 175 else 176 { 177 return null; 178 } 179 } 180 181 /** 182 * Test if the path is a repeater entry path (for example entries[1]) 183 * @param path the path representing the repeater entry 184 * @return true if the pathSegment is a repeater entry path 185 */ 186 public static boolean isRepeaterEntryPath(String path) 187 { 188 return REPEATER_ENTRY_PATTERN.matcher(path).matches(); 189 } 190 191 /** 192 * Retrieves the pair of repeater name and entry position of the given path segment 193 * Return <code>null</code> if the given path does not represent a repeater entry 194 * @param pathSegment the path segment representing the repeater entry 195 * @return the pair of repeater name and entry position 196 */ 197 public static Pair<String, Integer> getRepeaterNameAndEntryPosition(String pathSegment) 198 { 199 Matcher matcher = REPEATER_ENTRY_PATTERN.matcher(pathSegment); 200 if (matcher.matches()) 201 { 202 String repeaterName = matcher.group(1); 203 String entryPositionAsString = matcher.group(2); 204 return new ImmutablePair<>(repeaterName, Integer.parseInt(entryPositionAsString)); 205 } 206 else 207 { 208 return null; 209 } 210 } 211 212 /** 213 * Checks if there is a non empty value, for the data at the given path 214 * @param dataHolder the data holder 215 * @param dataPath path of the data 216 * @param context the context of the value to check 217 * @return <code>true</code> if the data at the given path is defined by the model, if there is a non empty value for the data, and if the type of this value matches the type of the definition. <code>false</code> otherwise 218 * @throws IllegalArgumentException if the given data path is null or empty 219 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 220 */ 221 public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, BadDataPathCardinalityException 222 { 223 if (context.getStatus().isPresent()) 224 { 225 ExternalizableDataStatus status = context.getStatus().get(); 226 if (ExternalizableDataStatus.LOCAL.equals(status)) 227 { 228 return dataHolder.hasLocalValue(dataPath); 229 } 230 else 231 { 232 return dataHolder.hasExternalValue(dataPath); 233 } 234 } 235 else 236 { 237 return dataHolder.hasValue(dataPath); 238 } 239 } 240 241 /** 242 * Checks if there is value, even empty, for the data at the given path 243 * @param dataHolder the data holder 244 * @param dataPath path of the data 245 * @param context the context of the value to check 246 * @return <code>true</code> if the data at the given path is defined by the model, if there is a value, even empty, for the data, and if the type of this value matches the type of the definition. <code>false</code> otherwise 247 * @throws IllegalArgumentException if the given data path is null or empty 248 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 249 */ 250 public static boolean hasValueOrEmpty(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, BadDataPathCardinalityException 251 { 252 if (context.getStatus().isPresent()) 253 { 254 ExternalizableDataStatus status = context.getStatus().get(); 255 if (ExternalizableDataStatus.LOCAL.equals(status)) 256 { 257 return dataHolder.hasLocalValueOrEmpty(dataPath); 258 } 259 else 260 { 261 return dataHolder.hasExternalValueOrEmpty(dataPath); 262 } 263 } 264 else 265 { 266 return dataHolder.hasValueOrEmpty(dataPath); 267 } 268 } 269 270 /** 271 * Retrieves the value at the given path for the data aware ametys object with given id 272 * @param <T> type of the value to retrieve 273 * @param ametysObjectId identifier of the data aware ametys object 274 * @param dataPath path of the data 275 * @return the value of the data or <code>null</code> if not exists or is empty. 276 * @throws IllegalArgumentException if the given data path is null or empty 277 * @throws UndefinedItemPathException if the given data path is not defined by the model 278 * @throws BadItemTypeException if the type defined by the model doesn't match the type of the stored value 279 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 280 */ 281 @Callable 282 public static <T> T getValue(String ametysObjectId, String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 283 { 284 AmetysObject ametysObject = _resolver.resolveById(ametysObjectId); 285 if (ametysObject instanceof ModelAwareDataAwareAmetysObject) 286 { 287 return ((ModelAwareDataAwareAmetysObject) ametysObject).getValue(dataPath); 288 } 289 else if (ametysObject instanceof ModelLessDataAwareAmetysObject) 290 { 291 return ((ModelLessDataAwareAmetysObject) ametysObject).getValue(dataPath); 292 } 293 else 294 { 295 String message = String.format("Unable to retrieve the value at path '%s' from the ametys object '%s': this ametys object is not data aware.", dataPath, ametysObjectId); 296 throw new IllegalArgumentException(message); 297 } 298 } 299 300 /** 301 * Retrieves the value of the data at the given path 302 * @param <T> type of the value to retrieve 303 * @param dataHolder the data holder 304 * @param dataPath path of the data 305 * @param context the context of the value to retrieve 306 * @return the value of the data or <code>null</code> if not exists or is empty. 307 * @throws IllegalArgumentException if the given data path is null or empty 308 * @throws UndefinedItemPathException if the given data path is not defined by the model 309 * @throws BadItemTypeException if the type defined by the model doesn't match the type of the stored value 310 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 311 */ 312 public static <T> T getValue(ModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 313 { 314 if (context.getStatus().isPresent()) 315 { 316 ExternalizableDataStatus status = context.getStatus().get(); 317 if (ExternalizableDataStatus.LOCAL.equals(status)) 318 { 319 return dataHolder.getLocalValue(dataPath); 320 } 321 else 322 { 323 return dataHolder.getExternalValue(dataPath); 324 } 325 } 326 else 327 { 328 return dataHolder.getValue(dataPath); 329 } 330 } 331 332 /** 333 * Sets the value of the data at the given path 334 * @param dataHolder the data holder 335 * @param dataPath path of the data 336 * @param value the value to set. Give <code>null</code> to empty the value. 337 * @param context context of the data to set 338 * @throws IllegalArgumentException if the given data path is null or empty 339 * @throws UndefinedItemPathException if the given data path is not defined by the model 340 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 341 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 342 */ 343 public static void setValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, Object value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 344 { 345 setValue(dataHolder, dataPath, value, context, false); 346 } 347 348 /** 349 * Sets the value of the data at the given path 350 * @param dataHolder the data holder 351 * @param dataPath path of the data 352 * @param value the value to set. Give <code>null</code> to empty the value. 353 * @param context context of the data to set 354 * @param forceStatus <code>true</code> to force the status to the given status context if defined 355 * @throws IllegalArgumentException if the given data path is null or empty 356 * @throws UndefinedItemPathException if the given data path is not defined by the model 357 * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 358 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 359 */ 360 public static void setValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, Object value, ValueContext context, boolean forceStatus) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException 361 { 362 if (context.getStatus().isPresent()) 363 { 364 ExternalizableDataStatus status = context.getStatus().get(); 365 if (ExternalizableDataStatus.LOCAL.equals(status)) 366 { 367 dataHolder.setLocalValue(dataPath, value); 368 } 369 else 370 { 371 dataHolder.setExternalValue(dataPath, value); 372 } 373 374 if (forceStatus) 375 { 376 dataHolder.setStatus(dataPath, status); 377 } 378 } 379 else 380 { 381 dataHolder.setValue(dataPath, value); 382 } 383 } 384 385 /** 386 * Removes the stored value of the data at the given path 387 * @param dataHolder the data holder 388 * @param dataPath path of the data 389 * @param context context of the data to remove 390 * @throws IllegalArgumentException if the given data path is null or empty 391 * @throws UnknownDataException if the value at the given data path does not exist 392 * @throws BadItemTypeException if the value of the parent of the given path is not an item container 393 * @throws UndefinedItemPathException if the given data path is not defined by the model 394 * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple 395 */ 396 public static void removeValue(ModifiableModelAwareDataHolder dataHolder, String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, UnknownDataException, BadDataPathCardinalityException 397 { 398 if (context.getStatus().isPresent()) 399 { 400 ExternalizableDataStatus status = context.getStatus().get(); 401 if (ExternalizableDataStatus.LOCAL.equals(status)) 402 { 403 dataHolder.removeLocalValue(dataPath); 404 } 405 else 406 { 407 dataHolder.removeExternalValue(dataPath); 408 } 409 } 410 else 411 { 412 dataHolder.removeValue(dataPath); 413 } 414 } 415 416 /** 417 * Creates a value context from the given {@link SynchronizationContext} 418 * @param dataHolder the data holder 419 * @param dataPath the path of the value needing context 420 * @param synchronizationContext the {@link SynchronizationContext} 421 * @return the created {@link ValueContext} 422 */ 423 public static ValueContext createValueContextFromSynchronizationContext(ModelAwareDataHolder dataHolder, String dataPath, SynchronizationContext synchronizationContext) 424 { 425 ModelItem modelItem = dataHolder.getDefinition(dataPath); 426 ValueContext valueContext = ValueContext.newInstance(); 427 428 // If the data is not externalizable at all, there is no status to set. 429 // This can have an impact during values synchronization (ex: metatada for externalizable data status would be removed) 430 boolean isDataExternalizableInAnyContext = getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder.getRootDataHolder(), modelItem); 431 if (isDataExternalizableInAnyContext) 432 { 433 // If data is externalizable (not necessary in the current context), the value context has to be set, taking current context into account 434 boolean isDataExternalizableInCurrentContext = getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder.getRootDataHolder(), modelItem, synchronizationContext.getExternalizableDataContext()); 435 if (ExternalizableDataStatus.EXTERNAL.equals(synchronizationContext.getStatusToSynchronize()) && isDataExternalizableInCurrentContext) 436 { 437 valueContext.withStatus(ExternalizableDataStatus.EXTERNAL); 438 } 439 else 440 { 441 valueContext.withStatus(ExternalizableDataStatus.LOCAL); 442 } 443 } 444 445 446 return valueContext; 447 } 448 449 /** 450 * Copies the source {@link DataHolder} to the given {@link ModifiableDataHolder} destination. 451 * @param source the source {@link DataHolder} 452 * @param destination the {@link ModifiableDataHolder} destination 453 */ 454 public static void copyTo(DataHolder source, ModifiableDataHolder destination) 455 { 456 for (String name : source.getDataNames()) 457 { 458 copyTo(source, destination, name); 459 } 460 } 461 462 /** 463 * Copy the data name of the source {@link DataHolder} to the given {@link ModifiableDataHolder} destination. 464 * @param source the source {@link DataHolder} 465 * @param destination the {@link ModifiableDataHolder} destination 466 * @param name the name of the data 467 */ 468 public static void copyTo(DataHolder source, ModifiableDataHolder destination, String name) 469 { 470 Object value = source instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) source).getValue(name) : ((ModelLessDataHolder) source).getValue(name); 471 472 if (value instanceof Composite composite) 473 { 474 ModifiableComposite compositeDestination = destination.getComposite(name, true); 475 composite.copyTo(compositeDestination); 476 } 477 else if (destination instanceof ModifiableModelAwareDataHolder modelAwareDestination) 478 { 479 if (value instanceof Repeater repeater) 480 { 481 ModifiableRepeater repeaterDestination = modelAwareDestination.getRepeater(name, true); 482 repeater.copyTo(repeaterDestination); 483 } 484 else 485 { 486 ModelItem modelItem = modelAwareDestination.getDefinition(name); 487 if (modelItem instanceof ElementDefinition definition && definition.isEditable()) 488 { 489 modelAwareDestination.setValue(name, value); 490 } 491 } 492 } 493 else if (destination instanceof ModifiableModelLessDataHolder modelLessDestination) 494 { 495 modelLessDestination.setValue(name, value); 496 } 497 } 498 499 /** 500 * Generates SAX events for the data comments in the given view 501 * @param dataHolder the {@link ModelAwareDataHolder} 502 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 503 * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events 504 * @param dataPath the data path corresponding of the current data holder. Can be <code>null</code> or empty. 505 * @throws SAXException if an error occurs during the SAX events generation 506 */ 507 public static void commentsToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, String dataPath) throws SAXException 508 { 509 for (ViewItem viewItem : viewItemAccessor.getViewItems()) 510 { 511 if (viewItem instanceof ModelViewItem) 512 { 513 String dataName = ((ModelViewItem) viewItem).getDefinition().getName(); 514 String newDataPath = StringUtils.isNotEmpty(dataPath) ? dataPath + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName; 515 516 if (dataHolder.hasComments(dataName)) 517 { 518 List<DataComment> comments = dataHolder.getComments(dataName); 519 520 AttributesImpl commentsAttrs = new AttributesImpl(); 521 commentsAttrs.addCDATAAttribute("path", newDataPath); 522 XMLUtils.startElement(contentHandler, "metadata", commentsAttrs); 523 524 int id = 1; 525 for (DataComment comment : comments) 526 { 527 AttributesImpl commentAttrs = new AttributesImpl(); 528 commentAttrs.addCDATAAttribute("id", String.valueOf(id++)); 529 commentAttrs.addCDATAAttribute("date", DateUtils.zonedDateTimeToString(comment.getDate())); 530 commentAttrs.addCDATAAttribute("author", comment.getAuthor()); 531 XMLUtils.createElement(contentHandler, "comment", commentAttrs, comment.getComment()); 532 } 533 534 XMLUtils.endElement(contentHandler, "metadata"); 535 } 536 537 if (viewItem instanceof ViewItemAccessor) 538 { 539 Object value = dataHolder.getValue(dataName); 540 if (value instanceof ModelAwareDataHolder) 541 { 542 commentsToSAX((ModelAwareDataHolder) value, contentHandler, (ViewItemAccessor) viewItem, newDataPath); 543 } 544 else if (value instanceof ModelAwareRepeater) 545 { 546 for (ModelAwareRepeaterEntry entry : ((ModelAwareRepeater) value).getEntries()) 547 { 548 String entryDataPath = newDataPath + "[" + entry.getPosition() + "]"; 549 commentsToSAX(entry, contentHandler, (ViewItemAccessor) viewItem, entryDataPath); 550 } 551 552 } 553 } 554 } 555 else if (viewItem instanceof ViewItemAccessor) 556 { 557 commentsToSAX(dataHolder, contentHandler, (ViewItemAccessor) viewItem, dataPath); 558 } 559 } 560 } 561 562 /** 563 * Generates SAX events for data contained in this {@link DataHolder} 564 * @param dataHolder the {@link ModelLessDataHolder} to SAX 565 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 566 * @param context The context of the data to SAX 567 * @throws SAXException if an error occurs during the SAX events generation 568 * @throws UnknownTypeException if there is no compatible type with the saxed value 569 * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the saxed value 570 */ 571 public static void dataToSAX(ModelLessDataHolder dataHolder, ContentHandler contentHandler, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException 572 { 573 for (String dataName : dataHolder.getDataNames()) 574 { 575 DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName); 576 dataToSAX(dataHolder, contentHandler, dataName, newContext); 577 } 578 } 579 580 /** 581 * Generates SAX events for data at the given relative path in this {@link DataHolder} 582 * @param dataHolder the {@link ModelLessDataHolder} to SAX 583 * @param contentHandler the {@link ContentHandler} that will receive the SAX events 584 * @param relativeDataPath the path of the data to SAX, relative to the given data holder 585 * @param context The context of the data to SAX 586 * @throws SAXException if an error occurs during the SAX events generation 587 * @throws UnknownTypeException if there is no compatible type with the saxed value 588 * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the saxed value 589 */ 590 public static void dataToSAX(ModelLessDataHolder dataHolder, ContentHandler contentHandler, String relativeDataPath, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException 591 { 592 String dataName = relativeDataPath; 593 if (relativeDataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 594 { 595 dataName = StringUtils.substringAfterLast(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR); 596 } 597 598 if (dataHolder.hasValue(relativeDataPath)) 599 { 600 ModelItemType type = dataHolder.getType(relativeDataPath); 601 Object value = dataHolder.getValueOfType(relativeDataPath, type.getId()); 602 type.valueToSAX(contentHandler, dataName, value, context); 603 } 604 else if (context.renderEmptyValues() && dataHolder.hasValueOrEmpty(relativeDataPath)) 605 { 606 XMLUtils.createElement(contentHandler, dataName); 607 } 608 } 609 610 /** 611 * Convert the data contained in the given {@link DataHolder} 612 * @param dataHolder the {@link ModelLessDataHolder} 613 * @param context The context of the data to convert 614 * @return The data of the given {@link DataHolder} as JSON 615 * @throws UnknownTypeException if there is no compatible type with the value to convert 616 * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the value to convert 617 */ 618 public static Map<String, Object> dataToJSON(ModelLessDataHolder dataHolder, DataContext context) throws UnknownTypeException, NotUniqueTypeException 619 { 620 Map<String, Object> result = new HashMap<>(); 621 for (String dataName : dataHolder.getDataNames()) 622 { 623 DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName); 624 result.put(dataName, dataToJSON(dataHolder, dataName, newContext)); 625 } 626 627 return result; 628 } 629 630 /** 631 * Convert the data at the given relative path into a JSON object 632 * @param dataHolder the {@link ModelLessDataHolder} 633 * @param relativeDataPath the path of the data to convert, relative to the given data holder 634 * @param context The context of the data to convert 635 * @return The value as JSON 636 * @throws UnknownTypeException if there is no compatible type with the value to convert 637 * @throws NotUniqueTypeException if there are many compatible types (there is no way to determine which type is the good one) with the value to convert 638 */ 639 public static Object dataToJSON(ModelLessDataHolder dataHolder, String relativeDataPath, DataContext context) throws UnknownTypeException, NotUniqueTypeException 640 { 641 ModelItemType type = dataHolder.getType(relativeDataPath); 642 Object value = dataHolder.getValueOfType(relativeDataPath, type.getId()); 643 return type.valueToJSONForClient(value, context); 644 } 645 646 /** 647 * Find all modifiable data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries. 648 * Properties are not modifiables, so they are excluded from the result 649 * @param <T> the actual type of the requested data. 650 * @param dataHolder the data holder. 651 * @param type the type identifier. 652 * @return a Map with all data paths as keys and corresponding data as values. 653 */ 654 public static <T extends Object> Map<String, T> findEditableItemsByType(ModelAwareDataHolder dataHolder, String type) 655 { 656 ViewItemAccessor viewItemAccessor = org.ametys.runtime.model.ViewHelper.createViewItemAccessor(dataHolder.getModel()); 657 return _findItemsByType(dataHolder, viewItemAccessor, type, false, StringUtils.EMPTY); 658 } 659 660 /** 661 * Find all data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries. 662 * @param <T> the actual type of the requested data. 663 * @param dataHolder the data holder. 664 * @param type the type identifier. 665 * @return a Map with all data paths as keys and corresponding data as values. 666 */ 667 public static <T extends Object> Map<String, T> findItemsByType(ModelAwareDataHolder dataHolder, String type) 668 { 669 ViewItemAccessor viewItemAccessor = org.ametys.runtime.model.ViewHelper.createViewItemAccessor(dataHolder.getModel()); 670 return _findItemsByType(dataHolder, viewItemAccessor, type, true, StringUtils.EMPTY); 671 } 672 673 /** 674 * Find all data of the given type on a {@link ModelAwareDataHolder}, including in composite or repeater entries. 675 * @param <T> the actual type of the requested data. 676 * @param dataHolder the data holder. 677 * @param viewItemAccessor the view items to restrict to 678 * @param type the type identifier. 679 * @return a Map with all data paths as keys and corresponding data as values. 680 */ 681 public static <T extends Object> Map<String, T> findItemsByType(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String type) 682 { 683 return _findItemsByType(dataHolder, viewItemAccessor, type, true, StringUtils.EMPTY); 684 } 685 686 private static <T extends Object> Map<String, T> _findItemsByType(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String type, boolean includeNotEditableItems, String prefix) 687 { 688 Map<String, T> attributes = new HashMap<>(); 689 690 ViewHelper.visitView(viewItemAccessor, 691 (element, definition) -> { 692 // simple element 693 if ((includeNotEditableItems || definition.isEditable()) && definition.getType().getId().equals(type)) 694 { 695 String name = definition.getName(); 696 attributes.put(prefix + name, dataHolder.getValue(name)); 697 } 698 }, 699 (group, definition) -> { 700 // composite 701 String name = definition.getName(); 702 ModelAwareComposite composite = dataHolder.getComposite(name); 703 if (composite != null) 704 { 705 attributes.putAll(_findItemsByType(composite, group, type, includeNotEditableItems, prefix + name + "/")); 706 } 707 }, 708 (group, definition) -> { 709 // repeater 710 String name = definition.getName(); 711 ModelAwareRepeater repeater = dataHolder.getRepeater(name); 712 if (repeater != null) 713 { 714 for (ModelAwareRepeaterEntry entry : repeater.getEntries()) 715 { 716 attributes.putAll(_findItemsByType(entry, group, type, includeNotEditableItems, prefix + name + "[" + entry.getPosition() + "]/")); 717 } 718 } 719 }, 720 group -> attributes.putAll(_findItemsByType(dataHolder, group, type, includeNotEditableItems, prefix))); 721 722 return attributes; 723 } 724 725 /** 726 * Retrieve the {@link ExternalizableDataProviderExtensionPoint} 727 * @return the {@link ExternalizableDataProviderExtensionPoint} 728 */ 729 public static ExternalizableDataProviderExtensionPoint getExternalizableDataProviderExtensionPoint() 730 { 731 return _externalizableDataProviderEP; 732 } 733 734 /** 735 * Returns the value of the synchronized value corresponding of the future status 736 * If the value is not a {@link SynchronizableValue}, the value itself is returned 737 * The future status is determined from the synchronizable value itself or from the context if needed. 738 * As the returned value is extracted from the synchronizable value, it can be an {@link UntouchedValue} 739 * @param value the synchronizable value 740 * @param dataHolder the data holder concerned by the value 741 * @param modelItem the model item corresponding to the value 742 * @param dataPath the current data path of the value. Can be empty if the data is in a repeater, in a not yet existing entry 743 * @param context the synchronization context, used to compute the future status of the value 744 * @return the value of the synchronized value corresponding of the future status 745 */ 746 public static Object getValueFromSynchronizableValue(Object value, ModelAwareDataHolder dataHolder, ModelItem modelItem, Optional<String> dataPath, SynchronizationContext context) 747 { 748 if (value instanceof SynchronizableValue) 749 { 750 SynchronizableValue syncValue = (SynchronizableValue) value; 751 Optional<ExternalizableDataStatus> valueStatus = Optional.empty(); 752 if (!getExternalizableDataProviderExtensionPoint().isDataExternalizable(dataHolder, modelItem, context.getExternalizableDataContext())) 753 { 754 // If the data is not externalizable, use the local value 755 valueStatus = Optional.of(ExternalizableDataStatus.LOCAL); 756 } 757 else if (syncValue.getExternalizableStatus() != null) 758 { 759 // If the given synchronizable value specifies the future status of the data, 760 // the value to validate is the corresponding one 761 valueStatus = Optional.of(syncValue.getExternalizableStatus()); 762 } 763 else if (dataPath.isPresent()) 764 { 765 // Check if the status of the data is already present in the repository 766 String[] pathSegments = StringUtils.split(dataPath.get(), ModelItem.ITEM_PATH_SEPARATOR); 767 ModelAwareDataHolder parentDataHolder; 768 String dataName; 769 if (pathSegments.length == 1) 770 { 771 parentDataHolder = dataHolder; 772 dataName = pathSegments[0]; 773 } 774 else 775 { 776 String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1); 777 parentDataHolder = dataHolder.getValue(parentPath); 778 dataName = pathSegments[pathSegments.length - 1]; 779 } 780 781 if (parentDataHolder != null && parentDataHolder.getRepositoryData().hasValue(dataName + ModelAwareDataHolder.STATUS_SUFFIX)) 782 { 783 // Use the current status if it is present 784 valueStatus = Optional.of(parentDataHolder.getStatus(dataName)); 785 } 786 else if (context.forceStatusIfNotPresent()) 787 { 788 valueStatus = Optional.of(context.getStatusToSynchronize()); 789 } 790 } 791 else if (context.forceStatusIfNotPresent()) 792 { 793 // The data does not exist yet, get the status from the context 794 valueStatus = Optional.of(context.getStatusToSynchronize()); 795 } 796 797 return syncValue.getValue(valueStatus); 798 } 799 else 800 { 801 return value; 802 } 803 } 804 805 /** 806 * Check if the given {@link ModelItem} is multivalued, either being itself mutlivalued or part of a repeater 807 * @param item the model item to check 808 * @return true if the model item is multivalued 809 */ 810 public static boolean isMultiple(ModelItem item) 811 { 812 ModelItem currentItem = item; 813 while (currentItem != null) 814 { 815 if (currentItem instanceof RepeaterDefinition || currentItem instanceof ElementDefinition && ((ElementDefinition) currentItem).isMultiple()) 816 { 817 return true; 818 } 819 820 currentItem = currentItem.getParent(); 821 } 822 823 return false; 824 } 825 826 /** 827 * Converts the given values according to the definitions in the given {@link ViewItemContainer} 828 * @param viewItemContainer the view item container 829 * @param values the values to convert 830 * @return the converted values 831 */ 832 @SuppressWarnings("unchecked") 833 public static Map<String, Object> convertValues(ViewItemContainer viewItemContainer, Map<String, Object> values) 834 { 835 if (values == null) 836 { 837 return null; 838 } 839 840 Map<String, Object> result = new HashMap<>(); 841 842 ViewHelper.visitView(viewItemContainer, 843 (element, definition) -> { 844 // simple element 845 String name = definition.getName(); 846 847 if (values.containsKey(name)) 848 { 849 Object value = convertValue(definition, values.get(name)); 850 result.put(name, value); 851 } 852 }, 853 (group, definition) -> { 854 // composite 855 String name = definition.getName(); 856 if (values.containsKey(name)) 857 { 858 result.put(name, convertValues(group, (Map<String, Object>) values.get(name))); 859 } 860 }, 861 (group, definition) -> { 862 // repeater 863 String name = definition.getName(); 864 if (values.containsKey(name)) 865 { 866 List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name); 867 868 Object newValue = null; 869 if (entries != null) 870 { 871 List<Map<String, Object>> newEntries = new ArrayList<>(); 872 873 for (int i = 0; i < entries.size(); i++) 874 { 875 newEntries.add(convertValues(group, entries.get(i))); 876 } 877 878 newValue = newEntries; 879 } 880 881 result.put(name, newValue); 882 } 883 }, 884 group -> result.putAll(convertValues(group, values))); 885 886 return result; 887 } 888 889 /** 890 * Converts the given value according to the given definition 891 * If the value is multiple, an array is retrieved with each value converted to the right type 892 * @param definition the definition 893 * @param value the value to convert 894 * @return the converted value 895 * @throws BadItemTypeException if the value to convert is not compatible with the data type 896 */ 897 public static Object convertValue(ElementDefinition definition, Object value) throws BadItemTypeException 898 { 899 if (value == null) 900 { 901 return null; 902 } 903 904 if (value instanceof UntouchedValue) 905 { 906 return value; 907 } 908 909 if (definition.isMultiple()) 910 { 911 if (value instanceof Collection) 912 { 913 return ((Collection) value).stream() 914 .map(v -> definition.getType().castValue(v)) 915 .toArray(i -> Array.newInstance(definition.getType().getManagedClass(), i)); 916 } 917 else if (value.getClass().isArray()) 918 { 919 Class<?> valueType = value.getClass().getComponentType(); 920 Stream<Object> valueStream; 921 if (!valueType.isPrimitive()) 922 { 923 valueStream = Arrays.stream((Object[]) value); 924 } 925 else if (valueType.equals(Boolean.TYPE)) 926 { 927 valueStream = Arrays.stream(ArrayUtils.toObject((boolean[]) value)); 928 } 929 else if (valueType.equals(Byte.TYPE)) 930 { 931 valueStream = Arrays.stream(ArrayUtils.toObject((byte[]) value)); 932 } 933 else if (valueType.equals(Character.TYPE)) 934 { 935 valueStream = Arrays.stream(ArrayUtils.toObject((char[]) value)); 936 } 937 else if (valueType.equals(Short.TYPE)) 938 { 939 valueStream = Arrays.stream(ArrayUtils.toObject((short[]) value)); 940 } 941 else if (valueType.equals(Integer.TYPE)) 942 { 943 valueStream = Arrays.stream(ArrayUtils.toObject((int[]) value)); 944 } 945 else if (valueType.equals(Long.TYPE)) 946 { 947 valueStream = Arrays.stream(ArrayUtils.toObject((long[]) value)); 948 } 949 else if (valueType.equals(Double.TYPE)) 950 { 951 valueStream = Arrays.stream(ArrayUtils.toObject((double[]) value)); 952 } 953 else if (valueType.equals(Float.TYPE)) 954 { 955 valueStream = Arrays.stream(ArrayUtils.toObject((float[]) value)); 956 } 957 else 958 { 959 throw new IllegalArgumentException(value + " cannot be converted to array"); 960 } 961 962 return valueStream.map(v -> definition.getType().castValue(v)).toArray(i -> (Object[]) Array.newInstance(definition.getType().getManagedClass(), i)); 963 } 964 965 throw new IllegalArgumentException(value + " cannot be converted to array"); 966 } 967 else 968 { 969 return definition.getType().castValue(value); 970 } 971 } 972 973 /** 974 * Aggregates multiple values of the data holders at the given path 975 * @param <T> type of the value to retrieve 976 * @param dataHolders the data holding the values 977 * @param dataPath the path of the data to retrieve 978 * @param managedClass class of the retrieved values 979 * @return aggregated multiple values 980 */ 981 @SuppressWarnings("unchecked") 982 public static <T> T aggregateMultipleValues(List<? extends ModelAwareDataHolder> dataHolders, String dataPath, Class managedClass) 983 { 984 // Put all the values in a list 985 List<Object> allValuesAsList = new ArrayList<>(); 986 for (ModelAwareDataHolder dataHolder : dataHolders) 987 { 988 Object value = dataHolder.getValue(dataPath, true); 989 if (value != null) 990 { 991 if (value.getClass().isArray()) 992 { 993 for (int i = 0; i < Array.getLength(value); i++) 994 { 995 allValuesAsList.add(Array.get(value, i)); 996 } 997 } 998 else 999 { 1000 allValuesAsList.add(value); 1001 } 1002 } 1003 } 1004 1005 Object[] array = (Object[]) Array.newInstance(managedClass, allValuesAsList.size()); 1006 return (T) allValuesAsList.toArray(array); 1007 } 1008}