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