001/* 002 * Copyright 2014 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.cms.workflow; 017 018import java.io.IOException; 019import java.lang.reflect.Array; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Optional; 031import java.util.Set; 032import java.util.stream.Stream; 033 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.commons.collections4.CollectionUtils; 036import org.apache.commons.lang3.ArrayUtils; 037import org.apache.commons.lang3.tuple.Pair; 038 039import org.ametys.cms.ObservationConstants; 040import org.ametys.cms.content.references.OutgoingReferences; 041import org.ametys.cms.content.references.OutgoingReferencesExtractor; 042import org.ametys.cms.contenttype.AttributeDefinition; 043import org.ametys.cms.contenttype.ContentAttributeDefinition; 044import org.ametys.cms.contenttype.ContentType; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.contenttype.ContentTypesHelper; 047import org.ametys.cms.contenttype.ContentValidator; 048import org.ametys.cms.data.ContentDataHelper; 049import org.ametys.cms.data.ContentSynchronizationContext; 050import org.ametys.cms.data.ContentSynchronizationResult; 051import org.ametys.cms.data.ContentValue; 052import org.ametys.cms.data.ReferencedContents; 053import org.ametys.cms.repository.Content; 054import org.ametys.cms.repository.ModifiableContent; 055import org.ametys.cms.repository.WorkflowAwareContent; 056import org.ametys.core.observation.Event; 057import org.ametys.core.observation.ObservationManager; 058import org.ametys.core.user.User; 059import org.ametys.core.user.UserIdentity; 060import org.ametys.core.user.UserManager; 061import org.ametys.core.util.DateUtils; 062import org.ametys.plugins.repository.AmetysRepositoryException; 063import org.ametys.plugins.repository.data.DataComment; 064import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 065import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 066import org.ametys.plugins.repository.data.holder.group.Repeater; 067import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 068import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 069import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 070import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode; 071import org.ametys.plugins.repository.data.holder.values.SynchronizationResult; 072import org.ametys.plugins.repository.data.holder.values.UntouchedValue; 073import org.ametys.plugins.repository.lock.LockHelper; 074import org.ametys.plugins.repository.lock.LockableAmetysObject; 075import org.ametys.plugins.repository.model.CompositeDefinition; 076import org.ametys.plugins.repository.model.RepeaterDefinition; 077import org.ametys.plugins.workflow.AbstractWorkflowComponent; 078import org.ametys.plugins.workflow.component.CheckRightsCondition; 079import org.ametys.runtime.authentication.AccessDeniedException; 080import org.ametys.runtime.config.Config; 081import org.ametys.runtime.i18n.I18nizableText; 082import org.ametys.runtime.i18n.I18nizableTextParameter; 083import org.ametys.runtime.model.ElementDefinition; 084import org.ametys.runtime.model.ModelHelper; 085import org.ametys.runtime.model.ModelItem; 086import org.ametys.runtime.model.ModelItemContainer; 087import org.ametys.runtime.model.View; 088import org.ametys.runtime.model.ViewHelper; 089import org.ametys.runtime.model.ViewItemContainer; 090import org.ametys.runtime.model.type.ElementType; 091import org.ametys.runtime.parameter.Errors; 092import org.ametys.runtime.parameter.Validator; 093 094import com.opensymphony.module.propertyset.PropertySet; 095import com.opensymphony.workflow.FunctionProvider; 096import com.opensymphony.workflow.WorkflowException; 097 098/** 099 * OSWorkflow function to edit a content.<br> 100 * <br> 101 * Values are set either programmatically, or parsed from form submission by their {@link ElementType}s according to the {@link Content} model.<br> 102 * <br> 103 * The required transient variables:<br> 104 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY - Map<String, Object> The map containing the results of the function.<br> 105 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.result - String "true" when everything goes fine. Missing in other case.<br> 106 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath> - Errors Each error during edition will be set here. Key will be the metadata path (with '.' separator). Value will be the error message.<br> 107 * - AbstractContentWorkflowComponent.CONTENT_KEY - WorkflowAwareContent The content that will be edited. Should have the lock token.<br> 108 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY - Map<String, Object> Contains the following parameters:<br> 109 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.QUIT - boolean True to specify edition mode will be quit, this imply to unlock the content.<br> 110 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.VIEW_PARAM The name of the view to use and to check attributes. If missing a view will be created from values. <br> 111 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FALLBACK_VIEW_PARAM The name of the view to use if the initial view does not exist on the Content's model. <br> 112 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.VALUES_KEY - Map<String, Object> The typed values. If present, raw values must not be present.<br> 113 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_VALUES - Map<String, Object> The values of the submitted form. If present, types values must not be present.<br> 114 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_VALUES.<MetadataPath> Object Key is the path of the metadata ('.' separated) prefixed by FORM_ELEMENTS_PREFIX. Value is a depending on the type of metadata. 115 * Sometimes types require additional information. In that case : Key is a metadata path ('.' separated) prefixed by INTERNAL_FORM_ELEMENTS_PREFIX and suffixed by '.' + an additional information name.<br> 116 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_COMMENTS - Map<String, List<Map<String, String>>> The comments of the metadata of the submitted form :<br> 117 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath> - List<Map<String, String>> Key is the path of the metadata ('.' separated) prefixed by FORM_ELEMENTS_PREFIX. Value is the list of comments.<br> 118 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X> - <Map<String, String> A comment with the following parameters<br> 119 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.author String The login of the author of the comment<br> 120 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.text String The text of the comment<br> 121 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.date String The date of the comment using the ISODateTimeFormat (See DateUtils.parse)<br> 122 * 123 * Where <MetadataPath> is the path of a metadata (using a '.' separator). In some cases it is prefixed by FORM_ELEMENTS_PREFIX. A metadata path with in a repeater include the number of the repeated instance (1 based).<br> 124 * Where <X> Is an element of the parent list.<br> 125 */ 126public class EditContentFunction extends AbstractContentWorkflowComponent implements FunctionProvider, Initializable 127{ 128 /** Constant for storing the action id for editing revert relations. */ 129 public static final String INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID = EditContentFunction.class.getName() + "$invertEditActionId"; 130 131 /** Constant for storing the action id for editing revert relations. */ 132 public static final String EDIT_MUTUAL_RELATIONSHIP = EditContentFunction.class.getName() + "$mutualRelationship"; 133 134 /** Prefix for HTML form elements. */ 135 public static final String FORM_ELEMENTS_PREFIX = "content.input."; 136 /** The key for global errors */ 137 public static final String GLOBAL_ERROR_KEY = "_global"; 138 /** Prefix for internal HTML form elements. */ 139 public static final String INTERNAL_FORM_ELEMENTS_PREFIX = "_" + FORM_ELEMENTS_PREFIX; 140 /** Inputs key for typed values. */ 141 public static final String VALUES_KEY = "typedValues"; 142 /** Request parameter key for the field values. */ 143 public static final String FORM_RAW_VALUES = "values"; 144 /** Request parameter key for the field comments. */ 145 public static final String FORM_RAW_COMMENTS = "comments"; 146 /** View parameter. */ 147 public static final String VIEW = "view"; 148 /** View items parameter. */ 149 public static final String VIEW_ITEMS = "view.items"; 150 /** View name parameter. */ 151 public static final String VIEW_NAME = "content.view"; 152 /** Fallback view name parameter. */ 153 public static final String FALLBACK_VIEW_NAME = "content.fallback.view"; 154 /** Quit edition mode parameter. */ 155 public static final String QUIT = "quit"; 156 /** Local only parameter. */ 157 public static final String LOCAL_ONLY = "local.only"; 158 /** Default action id of editing revert relations. */ 159 public static final int INVERT_EDIT_ACTION_ID = 2; 160 161 /** Content type extension point. */ 162 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 163 /** Helper for content types */ 164 protected ContentTypesHelper _contentTypesHelper; 165 /** Observation manager available to subclasses. */ 166 protected ObservationManager _observationManager; 167 /** The content workflow helper. */ 168 protected ContentWorkflowHelper _workflowHelper; 169 /** The outgoing references extractor */ 170 protected OutgoingReferencesExtractor _outgoingReferencesExtractor; 171 /** The user manager */ 172 protected UserManager _userManager; 173 /** Provider for externalizable data */ 174 protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 175 /** Helper for collecting content references */ 176 protected ContentDataHelper _contentDataHelper; 177 178 @Override 179 public void initialize() throws Exception 180 { 181 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) _manager.lookup(ContentTypeExtensionPoint.ROLE); 182 _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE); 183 _workflowHelper = (ContentWorkflowHelper) _manager.lookup(ContentWorkflowHelper.ROLE); 184 _contentTypesHelper = (ContentTypesHelper) _manager.lookup(ContentTypesHelper.ROLE); 185 _outgoingReferencesExtractor = (OutgoingReferencesExtractor) _manager.lookup(OutgoingReferencesExtractor.ROLE); 186 _userManager = (UserManager) _manager.lookup(UserManager.ROLE); 187 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) _manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 188 _contentDataHelper = (ContentDataHelper) _manager.lookup(ContentDataHelper.ROLE); 189 } 190 191 @SuppressWarnings("unchecked") 192 @Override 193 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 194 { 195 _logger.info("Performing edit workflow function"); 196 197 // Retrieve current content 198 WorkflowAwareContent content = getContent(transientVars); 199 UserIdentity user = getUser(transientVars); 200 201 // Get the action id for editing invert relations 202 int invertEditActionId = getInvertEditActionId(transientVars); 203 204 if (!(content instanceof ModifiableContent)) 205 { 206 throw new IllegalArgumentException("The provided content " + content.getId() + " is not a ModifiableContent."); 207 } 208 209 ModifiableContent modifiableContent = (ModifiableContent) content; 210 211 try 212 { 213 LockableAmetysObject lockableContent = _checkLock(content, user); 214 215 AllErrors errors = new AllErrors(); 216 217 Map<String, Object> parameters = getContextParameters(transientVars); 218 219 long time_0 = System.currentTimeMillis(); 220 221 // get inputs, either typed values (eg. set programmatically) 222 // or raw values (eg. from request parameters) 223 224 Map<String, Object> typedValues = (Map<String, Object>) parameters.get(VALUES_KEY); 225 Map<String, Object> rawValues = (Map<String, Object>) parameters.get(FORM_RAW_VALUES); 226 227 if (typedValues != null && rawValues != null) 228 { 229 throw new WorkflowException("Cannot have both typed values and raw values for EditContentFunction"); 230 } 231 232 if (typedValues == null && rawValues == null) 233 { 234 typedValues = Collections.EMPTY_MAP; 235 } 236 237 // get the view, either set from inputs or computed from values 238 View view = getView(parameters, typedValues, rawValues, modifiableContent); 239 240 long time_1 = System.currentTimeMillis(); 241 242 // get the attributes comments 243 Map<String, List<Map<String, String>>> rawComments = (Map<String, List<Map<String, String>>>) parameters.get(FORM_RAW_COMMENTS); 244 245 boolean localOnly = (boolean) parameters.getOrDefault(LOCAL_ONLY, false); 246 247 // get values 248 Map<String, Object> values = getValues(view, typedValues, modifiableContent, rawValues, rawComments, localOnly); 249 250 // validate values 251 validateValues(view, values, modifiableContent, errors); 252 globalValidate(modifiableContent, values, view, errors); 253 254 // prepare synchronize 255 // FIXME find external invert relations 256 Collection<ReferencedContents> referencedContents = prepareSynchronize(modifiableContent, view, values, invertEditActionId, user, errors); 257 258 _handleErrors(transientVars, modifiableContent, errors); 259 260 long time_2 = System.currentTimeMillis(); 261 262 // Notify the observers of the upcoming modification. 263 notifyContentModifying(content, values, transientVars); 264 265 // actually write changes 266 ContentSynchronizationContext context = ContentSynchronizationContext.newInstance() 267 .withStatus(ExternalizableDataStatus.LOCAL) 268 .withInvertRelations(invertRelationEnabled()) 269 .withReferencedContents(referencedContents); 270 271 SynchronizationResult synchronizationResult = modifiableContent.synchronizeValues(view, values, context); 272 273 // trigger edit workflow action on contents modified due to invert relations 274 if (synchronizationResult instanceof ContentSynchronizationResult) 275 { 276 ContentSynchronizationResult result = (ContentSynchronizationResult) synchronizationResult; 277 for (ModifiableContent refContent : result.getModifiedContents()) 278 { 279 refContent.saveChanges(); 280 _triggerEditWorkflowAction(refContent, invertEditActionId); 281 } 282 } 283 284 updateCommonMetadata(modifiableContent, user); 285 286 extractOutgoingReferences(modifiableContent); 287 288 long time_3 = System.currentTimeMillis(); 289 290 // Commit changes 291 modifiableContent.saveChanges(); 292 293 long time_4 = System.currentTimeMillis(); 294 295 // Notify the observers of the modification. 296 notifyContentModified(content, transientVars); 297 298 long time_5 = System.currentTimeMillis(); 299 300 // Unlock content if we are not in save & quit mode 301 Boolean quit = (Boolean) parameters.get(QUIT); 302 if (Boolean.TRUE.equals(quit) && lockableContent != null && lockableContent.isLocked()) 303 { 304 lockableContent.unlock(); 305 } 306 307 long time_6 = System.currentTimeMillis(); 308 309 boolean logAbnormalTime = Config.getInstance().getValue("runtime.log.abnormal.time"); 310 if (time_6 - time_0 > 5000 && logAbnormalTime) 311 { 312 _logger.warn("Edit content action has taken an abnormally long time : get view in " + (time_1 - time_0) + " ms / bind attributes in " + (time_2 - time_1) + " ms / build consistencies in " + (time_3 - time_2) + " ms / save in " + (time_4 - time_3) + " / notify listeners in " + (time_5 - time_4) + " / total in " + (time_6 - time_0) + " ms"); 313 } 314 else if (_logger.isDebugEnabled()) 315 { 316 _logger.debug("Edit timers : get view in " + (time_1 - time_0) + " ms / bind attributes in " + (time_2 - time_1) + " ms / build consistencies in " + (time_3 - time_2) + " ms / save in " + (time_4 - time_3) + " / notify listeners in " + (time_5 - time_4) + " / total in " + (time_6 - time_0) + " ms"); 317 } 318 319 getResultsMap(transientVars).put("result", "ok"); 320 } 321 catch (AmetysRepositoryException | AccessDeniedException | IOException e) 322 { 323 throw new WorkflowException("Unable to edit content " + modifiableContent + " from the repository", e); 324 } 325 } 326 327 private LockableAmetysObject _checkLock(Content content, UserIdentity user) throws WorkflowException 328 { 329 LockableAmetysObject lockableContent = null; 330 331 if (content instanceof LockableAmetysObject) 332 { 333 lockableContent = (LockableAmetysObject) content; 334 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 335 { 336 throw new WorkflowException("User '" + user + "' try to save content '" + content.getName() + "' but it is locked by another user"); 337 } 338 } 339 340 return lockableContent; 341 } 342 343 private void _handleErrors(Map transientVars, ModifiableContent modifiableContent, AllErrors errors) throws WorkflowException, InvalidInputWorkflowException 344 { 345 if (errors.hasErrors()) 346 { 347 // Populate the map to render 348 Map<String, Object> result = getResultsMap(transientVars); 349 350 Map<String, I18nizableText> errorFieldLabels = new HashMap<>(); 351 352 for (Map.Entry<String, Errors> entry : errors.getAllErrors().entrySet()) 353 { 354 String dataPath = entry.getKey(); 355 String canonicalMetadataPath = entry.getKey().replace('/', '.'); 356 357 result.put(canonicalMetadataPath, entry.getValue()); 358 359 if (modifiableContent.hasDefinition(dataPath)) 360 { 361 ModelItem definition = modifiableContent.getDefinition(dataPath); 362 errorFieldLabels.put(canonicalMetadataPath, definition.getLabel()); 363 } 364 } 365 366 result.put("errorFieldLabels", errorFieldLabels); 367 368 throw new InvalidInputWorkflowException("At least one validation error is preventing from saving the modifications", errors); 369 } 370 } 371 372 /** 373 * Get the identifier of the invert edit action 374 * @param transientVars The workflow vars 375 * @return the identifier of the invert edit action 376 */ 377 protected int getInvertEditActionId(Map transientVars) 378 { 379 return transientVars.containsKey(INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID) ? (Integer) transientVars.get(INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID) : INVERT_EDIT_ACTION_ID; 380 } 381 382 /** 383 * Notify observers that the content is being modified 384 * @param content The content being modified 385 * @param values the new values being set to the content 386 * @param transientVars The workflow vars 387 * @throws WorkflowException If an error occurred 388 */ 389 protected void notifyContentModifying(Content content, Map<String, Object> values, Map transientVars) throws WorkflowException 390 { 391 Map<String, Object> eventParams = new HashMap<>(); 392 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 393 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 394 eventParams.put(ObservationConstants.ARGS_CONTENT_VALUES, values); 395 396 if (transientVars.containsKey(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP)) 397 { 398 eventParams.put(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP, true); 399 } 400 401 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFYING, getUser(transientVars), eventParams)); 402 } 403 404 /** 405 * Notify observers that the content has been modified 406 * @param content The content modified 407 * @param transientVars The workflow vars 408 * @throws WorkflowException If an error occurred 409 */ 410 protected void notifyContentModified(Content content, Map transientVars) throws WorkflowException 411 { 412 Map<String, Object> eventParams = new HashMap<>(); 413 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 414 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 415 416 if (transientVars.containsKey(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP)) 417 { 418 eventParams.put(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP, true); 419 } 420 421 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, getUser(transientVars), eventParams)); 422 } 423 424 /** 425 * Get the view for the content 426 * @param parameters The parameters 427 * @param values Typed values from inputs 428 * @param rawValues The raw values of the form 429 * @param content The content 430 * @return The view asked in the request or a built-in view 431 * @throws WorkflowException If an error occurred while getting the view 432 */ 433 @SuppressWarnings("unchecked") 434 protected View getView(Map<String, Object> parameters, Map<String, Object> values, Map<String, Object> rawValues, Content content) throws WorkflowException 435 { 436 View view = (View) parameters.get(VIEW); 437 if (view == null) 438 { 439 List<String> viewItems = (List<String>) parameters.get(VIEW_ITEMS); 440 if (viewItems != null) 441 { 442 view = ViewHelper.createViewItemAccessor(content.getModel(), viewItems.toArray(String[]::new)); 443 } 444 else 445 { 446 String viewName = (String) parameters.get(VIEW_NAME); 447 String fallbackViewName = (String) parameters.get(FALLBACK_VIEW_NAME); 448 if (viewName != null) 449 { 450 view = _contentTypesHelper.getViewWithFallback(viewName, fallbackViewName, content.getTypes(), content.getMixinTypes()); 451 } 452 } 453 } 454 455 if (view != null) 456 { 457 if (ViewHelper.areItemsPresentsOnlyOnce(view)) 458 { 459 return ViewHelper.getTruncatedView(view); 460 } 461 else 462 { 463 throw new WorkflowException("The view '" + view.getName() + "' of content '" + content + "'cannot be used to edit the content because one or more items do not appear only once."); 464 } 465 } 466 467 // Compute a view from input values 468 String[] computedViewItems; 469 Collection<? extends ModelItemContainer> model = content.getModel(); 470 if (values != null) 471 { 472 Set<String> items; 473 items = new HashSet<>(); 474 _getViewItems(values, model, content, items); 475 computedViewItems = items.toArray(String[]::new); 476 } 477 else 478 { 479 computedViewItems = rawValues.keySet().stream() 480 .filter(path -> path.startsWith(FORM_ELEMENTS_PREFIX)) 481 .map(path -> path.substring(FORM_ELEMENTS_PREFIX.length())) 482 .map(ModelHelper::getDefinitionPathFromDataPath) 483 .toArray(String[]::new); 484 } 485 486 return ViewHelper.createViewItemAccessor(model, computedViewItems); 487 } 488 489 @SuppressWarnings("unchecked") 490 private void _getViewItems(Map<String, Object> values, Collection<? extends ModelItemContainer> parent, Content content, Set<String> viewItems) 491 { 492 for (String name : values.keySet()) 493 { 494 ModelItem modelItem = ModelHelper.getModelItem(name, parent); 495 String path = modelItem.getPath(); 496 497 if (modelItem instanceof AttributeDefinition) 498 { 499 if (((AttributeDefinition) modelItem).canWrite(content)) 500 { 501 viewItems.add(path); 502 } 503 } 504 else if (modelItem instanceof CompositeDefinition) 505 { 506 Object value = values.get(name); 507 508 if (!(value instanceof Map)) 509 { 510 throw new IllegalArgumentException("CompositeDefinition should correspond to a Map<String, Object> value."); 511 } 512 513 Map<String, Object> composite = (Map<String, Object>) value; 514 515 if (composite.isEmpty()) 516 { 517 // composite is empty, we should add it anyway so that the corresponding storage could also be emptied 518 viewItems.add(path); 519 } 520 else 521 { 522 _getViewItems(composite, Collections.singleton((CompositeDefinition) modelItem), content, viewItems); 523 } 524 } 525 else if (modelItem instanceof RepeaterDefinition) 526 { 527 Object value = values.get(name); 528 529 if (!(value instanceof List) && !(value instanceof SynchronizableRepeater)) 530 { 531 throw new IllegalArgumentException("RepeaterDefinition should correspond to a SynchronizableRepeater or List<Map<String, Object>> value."); 532 } 533 534 List<Map<String, Object>> entries = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getEntries() : (List<Map<String, Object>>) value; 535 536 if (entries.isEmpty()) 537 { 538 // repeater is empty, we should add it anyway so that the corresponding storage could also be emptied 539 viewItems.add(path); 540 } 541 else 542 { 543 for (int i = 0; i < entries.size(); i++) 544 { 545 Map<String, Object> entry = entries.get(i); 546 _getViewItems(entry, Collections.singleton((RepeaterDefinition) modelItem), content, viewItems); 547 } 548 } 549 } 550 } 551 } 552 553 /** 554 * Computes the actual typed values from the input. 555 * @param view the current {@link View} 556 * @param typedValues typed values, if any 557 * @param content the current Content 558 * @param rawValues raw values from form, if any 559 * @param rawComments the form comments, if any 560 * @param localOnly if the form values are local only or may include external values 561 * @return the actual values to be set 562 */ 563 protected Map<String, Object> getValues(View view, Map<String, Object> typedValues, ModifiableContent content, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, boolean localOnly) 564 { 565 Map<String, Object> values = typedValues; 566 if (values == null) 567 { 568 values = _parseValues(view, "", content, rawValues, rawComments, localOnly); 569 } 570 else 571 { 572 values = _convertValues(view, values); 573 } 574 575 return values; 576 } 577 578 @SuppressWarnings("unchecked") 579 private Map<String, Object> _parseValues(ViewItemContainer viewItemContainer, String prefix, ModifiableContent content, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, boolean localOnly) 580 { 581 Map<String, Object> values = new HashMap<>(); 582 583 org.ametys.plugins.repository.model.ViewHelper.visitView(viewItemContainer, 584 (element, definition) -> { 585 // simple element 586 String name = definition.getName(); 587 ElementType type = definition.getType(); 588 589 Object value; 590 if (!((AttributeDefinition) definition).canWrite(content)) 591 { 592 value = new UntouchedValue(); 593 } 594 else 595 { 596 Object initialValue = rawValues.get(FORM_ELEMENTS_PREFIX + prefix + name); 597 Object rawValue = initialValue; 598 ExternalizableDataStatus status = null; 599 Object externalValue = null; 600 601 // if the value is externalizable, rawValue is actually a Map {local:<value>, external:<value>, status:<local or external>} 602 if (!localOnly && _externalizableDataProviderEP.isDataExternalizable(content, definition)) 603 { 604 Map<String, Object> externalizableValue = (Map<String, Object>) initialValue; 605 606 status = ExternalizableDataStatus.valueOf(((String) externalizableValue.get("status")).toUpperCase()); 607 rawValue = externalizableValue.get("local"); 608 609 Object rawExternalValue = externalizableValue.get("external"); 610 externalValue = type.fromJSONForClient(rawExternalValue); 611 } 612 613 // get the typed value 614 Object typedValue = type.fromJSONForClient(rawValue); 615 616 // retrieve the associated comments 617 List<Map<String, String>> comments = rawComments == null ? null : rawComments.get(FORM_ELEMENTS_PREFIX + prefix + name); 618 619 value = _getSynchronizableValue(typedValue, status, externalValue, comments); 620 } 621 622 values.put(name, value); 623 }, 624 (group, definition) -> { 625 // composite 626 String name = definition.getName(); 627 values.put(name, _parseValues(group, prefix + name + "/", content, rawValues, rawComments, localOnly)); 628 }, 629 (group, definition) -> { 630 // repeater 631 String name = definition.getName(); 632 int size = (int) rawValues.get(INTERNAL_FORM_ELEMENTS_PREFIX + prefix + name + "/size"); 633 634 List<Map<String, Object>> entries = new ArrayList<>(); 635 Map<Integer, Integer> mapping = new HashMap<>(); 636 for (int i = 1; i <= size; i++) 637 { 638 int previousPosition = (int) rawValues.get(INTERNAL_FORM_ELEMENTS_PREFIX + prefix + name + "[" + i + "]/previous-position"); 639 if (previousPosition > 0) 640 { 641 mapping.put(previousPosition, i); 642 } 643 644 entries.add(_parseValues(group, prefix + name + "[" + i + "]/", content, rawValues, rawComments, localOnly)); 645 } 646 647 values.put(name, SynchronizableRepeater.replaceAll(entries, mapping)); 648 }, 649 group -> values.putAll(_parseValues(group, prefix, content, rawValues, rawComments, localOnly))); 650 651 return values; 652 } 653 654 private Object _getSynchronizableValue(Object value, ExternalizableDataStatus status, Object externalValue, List<Map<String, String>> rawComments) 655 { 656 SynchronizableValue result = new SynchronizableValue(value); 657 result.setExternalizableStatus(status != null ? status : ExternalizableDataStatus.LOCAL); 658 result.setExternalValue(externalValue); 659 660 if (rawComments != null) 661 { 662 List<DataComment> comments = new ArrayList<>(); 663 664 for (Map<String, String> rawComment : rawComments) 665 { 666 String author = rawComment.get("author"); 667 String text = rawComment.get("text"); 668 String rawDate = rawComment.get("date"); 669 670 DataComment comment = new DataComment(text, DateUtils.parseZonedDateTime(rawDate), author); 671 672 comments.add(comment); 673 } 674 675 result.setComments(comments); 676 } 677 678 return result; 679 } 680 681 private Object _convertValue(ElementDefinition definition, Object value) 682 { 683 if (value == null) 684 { 685 return null; 686 } 687 688 if (definition.isMultiple()) 689 { 690 if (value instanceof Collection) 691 { 692 return ((Collection) value).stream().map(v -> definition.getType().castValue(v)).toArray(i -> Array.newInstance(definition.getType().getManagedClass(), i)); 693 } 694 else if (value.getClass().isArray()) 695 { 696 Class<?> valueType = value.getClass().getComponentType(); 697 Stream<Object> valueStream; 698 if (!valueType.isPrimitive()) 699 { 700 valueStream = Arrays.stream((Object[]) value); 701 } 702 else if (valueType.equals(Boolean.TYPE)) 703 { 704 valueStream = Arrays.stream(ArrayUtils.toObject((boolean[]) value)); 705 } 706 else if (valueType.equals(Byte.TYPE)) 707 { 708 valueStream = Arrays.stream(ArrayUtils.toObject((byte[]) value)); 709 } 710 else if (valueType.equals(Character.TYPE)) 711 { 712 valueStream = Arrays.stream(ArrayUtils.toObject((char[]) value)); 713 } 714 else if (valueType.equals(Short.TYPE)) 715 { 716 valueStream = Arrays.stream(ArrayUtils.toObject((short[]) value)); 717 } 718 else if (valueType.equals(Integer.TYPE)) 719 { 720 valueStream = Arrays.stream(ArrayUtils.toObject((int[]) value)); 721 } 722 else if (valueType.equals(Long.TYPE)) 723 { 724 valueStream = Arrays.stream(ArrayUtils.toObject((long[]) value)); 725 } 726 else if (valueType.equals(Double.TYPE)) 727 { 728 valueStream = Arrays.stream(ArrayUtils.toObject((double[]) value)); 729 } 730 else if (valueType.equals(Float.TYPE)) 731 { 732 valueStream = Arrays.stream(ArrayUtils.toObject((float[]) value)); 733 } 734 else 735 { 736 throw new IllegalArgumentException(value + " cannot be converted to array"); 737 } 738 739 return valueStream.map(v -> definition.getType().castValue(v)).toArray(i -> (Object[]) Array.newInstance(definition.getType().getManagedClass(), i)); 740 } 741 742 throw new IllegalArgumentException(value + " cannot be converted to array"); 743 } 744 else 745 { 746 return definition.getType().castValue(value); 747 } 748 } 749 750 @SuppressWarnings("unchecked") 751 private Map<String, Object> _convertValues(ViewItemContainer viewItemContainer, Map<String, Object> values) 752 { 753 if (values == null) 754 { 755 return null; 756 } 757 758 Map<String, Object> result = new HashMap<>(); 759 760 org.ametys.plugins.repository.model.ViewHelper.visitView(viewItemContainer, 761 (element, definition) -> { 762 // simple element 763 String name = definition.getName(); 764 765 if (values.containsKey(name)) 766 { 767 Object value = values.get(name); 768 SynchronizableValue syncValue = value instanceof SynchronizableValue ? (SynchronizableValue) value : null; 769 value = syncValue == null ? value : syncValue.getValue(); 770 771 value = _convertValue(definition, value); 772 773 if (syncValue != null) 774 { 775 syncValue.setValue(value); 776 syncValue.setExternalValue(_convertValue(definition, syncValue.getExternalValue())); 777 } 778 779 SynchronizableValue newValue = syncValue == null ? new SynchronizableValue(value) : syncValue; 780 result.put(name, newValue); 781 } 782 }, 783 (group, definition) -> { 784 // composite 785 String name = definition.getName(); 786 if (values.containsKey(name)) 787 { 788 result.put(name, _convertValues(group, (Map<String, Object>) values.get(name))); 789 } 790 }, 791 (group, definition) -> { 792 // repeater 793 String name = definition.getName(); 794 if (values.containsKey(name)) 795 { 796 Object value = values.get(name); 797 SynchronizableRepeater syncRepeater = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : null; 798 List<Map<String, Object>> entries = value == null ? null : value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries(); 799 800 Object newValue = null; 801 if (entries != null) 802 { 803 List<Map<String, Object>> newEntries = new ArrayList<>(); 804 805 for (int i = 0; i < entries.size(); i++) 806 { 807 newEntries.add(_convertValues(group, entries.get(i))); 808 } 809 810 newValue = newEntries; 811 812 if (syncRepeater != null) 813 { 814 newValue = SynchronizableRepeater.copy(syncRepeater, newEntries); 815 } 816 } 817 818 result.put(name, newValue); 819 } 820 }, 821 group -> result.putAll(_convertValues(group, values))); 822 823 return result; 824 } 825 826 /** 827 * Validates all input values. 828 * @param view the model's view corresponding to the values 829 * @param values the actual input values 830 * @param content the current content 831 * @param allErrors object to be populated with validation errors 832 * @throws WorkflowException If an error occurred 833 */ 834 protected void validateValues(View view, Map<String, Object> values, ModifiableContent content, AllErrors allErrors) throws WorkflowException 835 { 836 _validateValues(view, Optional.of(values), "", content, allErrors); 837 } 838 839 @SuppressWarnings("unchecked") 840 private void _validateValues(ViewItemContainer viewItemContainer, Optional<Map<String, Object>> values, String dataPath, ModifiableContent content, AllErrors allErrors) 841 { 842 org.ametys.plugins.repository.model.ViewHelper.visitView(viewItemContainer, 843 (element, definition) -> { 844 // simple element 845 String name = definition.getName(); 846 validateValue(definition, content, dataPath + name, allErrors, values.map(v -> v.get(name)).orElse(null)); 847 }, 848 (group, definition) -> { 849 // composite 850 String name = definition.getName(); 851 Optional<Map<String, Object>> value = values.map(v -> v.get(name)).filter(Map.class::isInstance).map(Map.class::cast); 852 853 _validateValues(group, value, dataPath + name + "/", content, allErrors); 854 }, 855 (group, definition) -> { 856 // repeater 857 String name = definition.getName(); 858 Object value = values.map(v -> v.get(name)).orElse(null); 859 860 List<Map<String, Object>> entries = value == null ? null : value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries(); 861 SynchronizableRepeater.Mode mode = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getMode() : SynchronizableRepeater.Mode.REPLACE_ALL; 862 863 int oldRepeaterSize = 0; 864 if (mode != SynchronizableRepeater.Mode.REPLACE_ALL) 865 { 866 Repeater repeater = content.getRepeater(dataPath + name); 867 if (repeater != null) 868 { 869 oldRepeaterSize = repeater.getSize(); 870 } 871 } 872 873 int repeaterSize = entries != null ? entries.size() : 0; 874 875 if (mode == SynchronizableRepeater.Mode.APPEND) 876 { 877 SynchronizableRepeater repeater = (SynchronizableRepeater) value; 878 assert repeater != null; 879 repeaterSize = oldRepeaterSize + repeaterSize - repeater.getRemovedEntries().size(); 880 } 881 else if (mode == SynchronizableRepeater.Mode.REPLACE) 882 { 883 repeaterSize = oldRepeaterSize; 884 } 885 886 int minSize = definition.getMinSize(); 887 int maxSize = definition.getMaxSize(); 888 889 if (repeaterSize < minSize) 890 { 891 Errors errors = new Errors(); 892 893 List<String> parameters = new ArrayList<>(); 894 parameters.add(name); 895 parameters.add(Integer.toString(minSize)); 896 errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_REPEATER_MINSIZE", parameters)); 897 allErrors.addError(dataPath + name, errors); 898 } 899 900 if (maxSize > 0 && repeaterSize > maxSize) 901 { 902 Errors errors = new Errors(); 903 904 List<String> parameters = new ArrayList<>(); 905 parameters.add(name); 906 parameters.add(Integer.toString(maxSize)); 907 errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_REPEATER_MAXSIZE", parameters)); 908 allErrors.addError(dataPath + name, errors); 909 } 910 911 if (entries != null) 912 { 913 for (int i = 0; i < entries.size(); i++) 914 { 915 Map<String, Object> entry = entries.get(i); 916 _validateValues(group, Optional.of(entry), dataPath + name + "[" + (i + 1) + "]/", content, allErrors); 917 } 918 } 919 }, 920 group -> _validateValues(group, values, dataPath, content, allErrors)); 921 } 922 923 /** 924 * Validate an attribute value. 925 * @param definition the attribute definition. 926 * @param content the Content being edited. 927 * @param dataPath the attribute path. 928 * @param allErrors the errors. 929 * @param value the value. 930 */ 931 protected void validateValue(ElementDefinition definition, ModifiableContent content, String dataPath, AllErrors allErrors, Object value) 932 { 933 Object actualValue = DataHolderHelper.getValueToValidate(value); 934 Mode mode = value instanceof SynchronizableValue ? ((SynchronizableValue) value).getMode() : null; 935 936 if (mode == null) 937 { 938 mode = Mode.REPLACE; 939 } 940 941 if (actualValue instanceof UntouchedValue) 942 { 943 // don't validate UntouchedValue, either they correspond to non-writable or previously stored data 944 return; 945 } 946 947 if (!((AttributeDefinition) definition).canWrite(content)) 948 { 949 throw new AccessDeniedException("Current user has no right to edit attribute " + definition.getPath()); 950 } 951 952 Validator validator = definition.getValidator(); 953 Object valueToValidate = actualValue; 954 if (validator != null && validator.getClass().isAnnotationPresent(NeedAllValues.class)) 955 { 956 // the validator need all attribute values 957 Object oldValue = content.getValue(dataPath); 958 if (definition.isMultiple()) 959 { 960 Object[] oldValuesArray = (Object[]) oldValue; 961 Object[] newValuesArray = (Object[]) actualValue; 962 valueToValidate = mode == Mode.REPLACE ? actualValue : mode == Mode.APPEND ? ArrayUtils.addAll(oldValuesArray, newValuesArray) : CollectionUtils.disjunction(Arrays.asList(oldValuesArray), Arrays.asList(newValuesArray)).toArray(i -> (Object[]) Array.newInstance(definition.getType().getManagedClass(), i)); 963 } 964 else 965 { 966 valueToValidate = mode != Mode.REMOVE ? actualValue : null; 967 } 968 } 969 970 List<I18nizableText> errors = ModelHelper.validateValue(definition, valueToValidate); 971 972 if (errors != null && !errors.isEmpty()) 973 { 974 Errors e = new Errors(); 975 e.addErrors(errors); 976 allErrors.addError(dataPath, e); 977 } 978 } 979 980 /** 981 * Performs a global validation of the Content, based on declared {@link ContentValidator}s. 982 * @param content the current {@link Content}. 983 * @param values the values being set 984 * @param view the current {@link View} 985 * @param allErrors object to be populated with validation errors 986 */ 987 protected void globalValidate(Content content, Map<String, Object> values, View view, AllErrors allErrors) 988 { 989 Errors errors = new Errors(); 990 991 String[] allContentTypes = ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 992 993 for (String cTypeId : allContentTypes) 994 { 995 ContentType contentType = _contentTypeExtensionPoint.getExtension(cTypeId); 996 997 for (ContentValidator validator : contentType.getGlobalValidators()) 998 { 999 validator.validate(content, values, view, errors); 1000 } 1001 } 1002 1003 if (errors.hasErrors()) 1004 { 1005 // Global error 1006 allErrors.addError(GLOBAL_ERROR_KEY, errors); 1007 } 1008 } 1009 1010 /** 1011 * Prepares the write process by checking remote contents concerned by invert relations. 1012 * @param content the current content. 1013 * @param view the current View. 1014 * @param values the new values. 1015 * @param invertEditActionId the action id to check 1016 * @param user the current user 1017 * @param allErrors the collected errors 1018 * @return the {@link ReferencedContents} 1019 */ 1020 protected Collection<ReferencedContents> prepareSynchronize(ModifiableContent content, View view, Map<String, Object> values, int invertEditActionId, UserIdentity user, AllErrors allErrors) 1021 { 1022 if (!invertRelationEnabled()) 1023 { 1024 return null; 1025 } 1026 1027 Collection<ReferencedContents> referencedContents = _contentDataHelper.collectReferencedContents(view, content, values); 1028 1029 Map<ContentValue, Pair<Boolean, String>> refContents = new HashMap<>(); 1030 1031 // "flatten" the data, so that we only lock each content once 1032 // for each ref content, we only keep the first dataPath (for error reporting) and the weakest value for forceInvert (ie. false if any) 1033 for (ReferencedContents referencedContent : referencedContents) 1034 { 1035 ContentAttributeDefinition definition = referencedContent.getDefinition(); 1036 boolean forceInvert = definition.getForceInvert(); 1037 1038 _flattenCollectedReferencedContents(referencedContent.getAddedContentsWithPaths(), refContents, forceInvert); 1039 _flattenCollectedReferencedContents(referencedContent.getRemovedContentsWithPaths(), refContents, forceInvert); 1040 } 1041 1042 for (Entry<ContentValue, Pair<Boolean, String>> value : refContents.entrySet()) 1043 { 1044 ContentValue refContentValue = value.getKey(); 1045 ModifiableContent refContent = refContentValue.getContentIfExists().orElse(null); 1046 1047 if (refContent != null) 1048 { 1049 // Check if edit action in available on referenced contents 1050 if (_isEditRefContentAvailable(invertEditActionId, refContent, value.getValue().getLeft(), value.getValue().getRight(), user, allErrors)) 1051 { 1052 if (refContent instanceof LockableAmetysObject && !((LockableAmetysObject) refContent).isLocked()) 1053 { 1054 // Get lock on referenced content 1055 ((LockableAmetysObject) refContent).lock(); 1056 } 1057 } 1058 } 1059 } 1060 1061 return referencedContents; 1062 } 1063 1064 private void _flattenCollectedReferencedContents(Map<ContentValue, List<String>> references, Map<ContentValue, Pair<Boolean, String>> refContents, boolean forceInvert) 1065 { 1066 for (Entry<ContentValue, List<String>> value : references.entrySet()) 1067 { 1068 ContentValue refContentValue = value.getKey(); 1069 List<String> dataPaths = value.getValue(); 1070 1071 Pair<Boolean, String> invertData = refContents.get(refContentValue); 1072 if (invertData == null) 1073 { 1074 String firstData = dataPaths.isEmpty() ? "" : dataPaths.get(0); 1075 refContents.put(refContentValue, Pair.of(forceInvert, firstData)); 1076 } 1077 else if (!forceInvert && invertData.getLeft()) 1078 { 1079 String firstData = dataPaths.isEmpty() ? "" : dataPaths.get(0); 1080 refContents.put(refContentValue, Pair.of(forceInvert, firstData)); 1081 } 1082 } 1083 } 1084 1085 private boolean _isEditRefContentAvailable(int editActionId, Content refContent, boolean forceInvert, String currentMetadataPath, UserIdentity user, AllErrors allErrors) 1086 { 1087 if (refContent instanceof WorkflowAwareContent) 1088 { 1089 Map<String, Object> inputs = new HashMap<>(); 1090 if (forceInvert) 1091 { 1092 // do not check user's right 1093 inputs.put(CheckRightsCondition.FORCE, true); 1094 } 1095 1096 int[] availableActions = _workflowHelper.getAvailableActions((WorkflowAwareContent) refContent, inputs); 1097 if (!ArrayUtils.contains(availableActions, editActionId)) 1098 { 1099 Errors errors = new Errors(); 1100 Map<String, I18nizableTextParameter> params = new HashMap<>(); 1101 1102 // Check lock 1103 if (refContent instanceof LockableAmetysObject) 1104 { 1105 LockableAmetysObject lockableContent = (LockableAmetysObject) refContent; 1106 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 1107 { 1108 User lockOwner = _userManager.getUser(lockableContent.getLockOwner().getPopulationId(), lockableContent.getLockOwner().getLogin()); 1109 1110 params.put("content", new I18nizableText(_contentHelper.getTitle(refContent))); 1111 params.put("lockOwner", new I18nizableText(lockOwner != null ? lockOwner.getFullName() + " (" + lockOwner.getIdentity().getLogin() + ")" : lockableContent.getLockOwner().getLogin())); 1112 errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_REFERENCED_CONTENT_LOCKED", params)); 1113 allErrors.addError(currentMetadataPath, errors); 1114 1115 return false; 1116 } 1117 } 1118 1119 // Action in unavailable 1120 params.put("content", new I18nizableText(_contentHelper.getTitle(refContent))); 1121 errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_UNAVAILABLE_ACTION", params)); 1122 allErrors.addError(currentMetadataPath, errors); 1123 1124 return false; 1125 } 1126 else 1127 { 1128 return true; 1129 } 1130 } 1131 else 1132 { 1133 Errors errors = new Errors(); 1134 Map<String, I18nizableTextParameter> params = new HashMap<>(); 1135 params.put("content", new I18nizableText(_contentHelper.getTitle(refContent))); 1136 errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_NOWORKFLOWAWARE_CONTENT", params)); 1137 allErrors.addError(currentMetadataPath, errors); 1138 return false; 1139 } 1140 } 1141 1142 /** 1143 * Analyze the content to extract outgoing references and store them 1144 * @param content The content to analyze 1145 */ 1146 protected void extractOutgoingReferences(ModifiableContent content) 1147 { 1148 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content); 1149 content.setOutgoingReferences(outgoingReferencesByPath); 1150 } 1151 1152 /** 1153 * Template method to indicates if invert relation should be taken into account during the whole edition. 1154 * Override and return false to disabled invert relation management. 1155 * @return true if invert relation are enabled 1156 */ 1157 protected boolean invertRelationEnabled() 1158 { 1159 return true; 1160 } 1161 1162 /** 1163 * Updates common metadata (last contributor, last modification date, ...). 1164 * @param content the content. 1165 * @param user the user. 1166 * @throws WorkflowException if an error occurs. 1167 */ 1168 protected void updateCommonMetadata(ModifiableContent content, UserIdentity user) throws WorkflowException 1169 { 1170 if (user != null) 1171 { 1172 content.setLastContributor(user); 1173 } 1174 1175 content.setLastModified(new Date()); 1176 1177 if (content instanceof WorkflowAwareContent) 1178 { 1179 // Remove the proposal date. 1180 ((WorkflowAwareContent) content).setProposalDate(null); 1181 } 1182 } 1183 1184 /** 1185 * Trigger a 'edit content' workflow action (if the content is workflow-aware). 1186 * @param content The content. 1187 * @param actionId The current 'edit content' action ID. 1188 * @throws WorkflowException if an error occurs. 1189 */ 1190 protected void _triggerEditWorkflowAction(Content content, int actionId) throws WorkflowException 1191 { 1192 if (content instanceof WorkflowAwareContent) 1193 { 1194 Map<String, Object> inputs = new HashMap<>(); 1195 Map<String, Object> parameters = new HashMap<>(); 1196 1197 inputs.put(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP, true); 1198 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters); 1199 1200 // Do action regarless of user's rights because user's rights was already checked during preparing process 1201 // This is necessary because the removal of a invert relation could removed the user rights in a referenced content, whereas user has authorized to edit the content before this removal 1202 inputs.put(CheckRightsCondition.FORCE, true); 1203 1204 parameters.put(FORM_RAW_VALUES, Collections.EMPTY_MAP); // No values 1205 parameters.put(QUIT, true); 1206 1207 _workflowHelper.editContent((WorkflowAwareContent) content, null, actionId); 1208 } 1209 } 1210}