001/* 002 * Copyright 2021 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.contentio.synchronize.workflow; 017 018import java.util.HashMap; 019import java.util.Map; 020import java.util.Optional; 021import java.util.Set; 022 023import org.slf4j.Logger; 024 025import org.ametys.cms.ObservationConstants; 026import org.ametys.cms.data.ContentSynchronizationContext; 027import org.ametys.cms.data.ContentSynchronizationResult; 028import org.ametys.cms.repository.Content; 029import org.ametys.cms.repository.ModifiableContent; 030import org.ametys.cms.workflow.AbstractContentFunction; 031import org.ametys.cms.workflow.EditContentFunction; 032import org.ametys.core.observation.Event; 033import org.ametys.core.user.UserIdentity; 034import org.ametys.core.util.AvalonLoggerAdapter; 035import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 036import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDataProvider; 037import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 038import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 039import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper.SynchronizableValuesStatusInfos; 040import org.ametys.plugins.repository.data.holder.values.SynchronizationResult; 041import org.ametys.runtime.i18n.I18nizableText; 042import org.ametys.runtime.model.ModelItem; 043import org.ametys.runtime.model.View; 044import org.ametys.runtime.parameter.ValidationResult; 045import org.ametys.runtime.parameter.ValidationResults; 046 047import com.opensymphony.workflow.WorkflowException; 048 049/** 050 * OSWorkflow function to edit (import or synchronize) a synchronized content. 051 */ 052public class EditSynchronizedContentFunction extends EditContentFunction 053{ 054 /** Constant for storing the action id for editing invert relations on synchronized contents. */ 055 public static final String SYNCHRO_INVERT_EDIT_ACTION_ID_KEY = EditSynchronizedContentFunction.class.getName() + "$synchroInvertEditActionId"; 056 /** Constant for storing the action id for editing invert relations. */ 057 public static final String INVERT_EDIT_ACTION_ID_KEY = EditSynchronizedContentFunction.class.getName() + "$invertEditActionId"; 058 059 /** Constant for storing the ids of the contents related to the current content but that are not part of the synchronization **/ 060 public static final String NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY = EditSynchronizedContentFunction.class.getName() + "$notSynchronizedContents"; 061 062 /** Constant for storing the scc into the transient variables map. */ 063 public static final String SCC_KEY = SynchronizableContentsCollection.class.getName(); 064 /** Constant for storing the scc logger into the transient variables map. */ 065 public static final String SCC_LOGGER_KEY = SynchronizableContentsCollection.class.getName() + "$logger"; 066 /** Constant for storing the import parameters into the transient variables map. */ 067 public static final String ADDITIONAL_PARAMS_KEY = SynchronizableContentsCollection.class.getName() + "$additionalParams"; 068 069 /** Default action id of editing invert relations on synchronized contents. */ 070 public static final int DEFAULT_SYNCHRO_INVERT_EDIT_ACTION_ID = 800; 071 /** Default action id of editing revert relations. */ 072 public static final int DEFAULT_INVERT_EDIT_ACTION_ID = 22; 073 074 /** 075 * Import mode parameter. 076 * If the parameter is present and set to <code>true</code>, the content is considered as imported. 077 * Otherwise, it is considered as synchronized. 078 */ 079 public static final String IMPORT = "import"; 080 081 @Override 082 protected ContentSynchronizationContext getSynchronizationContext(Map transientVars) 083 { 084 ContentSynchronizationContext synchronizationContext = super.getSynchronizationContext(transientVars) 085 .withStatus(ExternalizableDataStatus.EXTERNAL); 086 087 getSynchronizableContentsCollection(transientVars) 088 .map(SynchronizableContentsCollection::getId) 089 .ifPresent(sccId -> synchronizationContext.withExternalizableDataContextEntry(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, sccId)); 090 091 return synchronizationContext; 092 } 093 094 @Override 095 protected Map<String, Object> _convertValues(ModifiableContent content, View view, Map<String, Object> values, Map transientVars) 096 { 097 Map<String, Object> externalizableContext = new HashMap<>(); 098 getSynchronizableContentsCollection(transientVars) 099 .map(SynchronizableContentsCollection::getId) 100 .ifPresent(sccId -> externalizableContext.put(SynchronizableContentsCollectionDataProvider.SCC_ID_CONTEXT_KEY, sccId)); 101 102 SynchronizableValuesStatusInfos statusInfos = new SynchronizableValuesStatusInfos(content, externalizableContext); 103 104 return DataHolderHelper.convertValuesWithSynchronizableValues(view, values, DataHolderHelper::convertValueIgnoringIncompatibleOnes, Optional.of(statusInfos)); 105 } 106 107 @Override 108 protected ValidationResults validateValues(View view, ModifiableContent content, Map<String, Object> values, Map transientVars) throws WorkflowException 109 { 110 // Do not validate synchronized values. 111 // Sometimes want to be able to get invalid data at the end of the synchronization. 112 // The validation workflow will fail and the content won't be valid anymore. That's normal 113 return new ValidationResults(); 114 } 115 116 @Override 117 protected ValidationResult globalValidate(View view, Content content, Map<String, Object> values) 118 { 119 // Do not validate synchronized values. 120 // Sometimes want to be able to get invalid data at the end of the synchronization. 121 // The validation workflow will fail and the content won't be valid anymore. That's normal 122 return ValidationResult.empty(); 123 } 124 125 @Override 126 protected ContentSynchronizationResult additionalOperations(ModifiableContent content, Map transientVars) throws WorkflowException 127 { 128 ContentSynchronizationResult result = super.additionalOperations(content, transientVars); 129 130 Optional<SynchronizableContentsCollection> scc = getSynchronizableContentsCollection(transientVars); 131 // There is no SCC in transient var when the modification is due to an invert relation 132 if (scc.isPresent()) 133 { 134 Map<String, Object> additionalParams = getAdditionalParameters(transientVars); 135 ContentSynchronizationResult additionalResult; 136 137 // Do the SCC additional operations 138 if (isImportMode(transientVars)) 139 { 140 additionalResult = scc.get().additionalImportOperations(content, additionalParams, getLogger(transientVars)); 141 } 142 else 143 { 144 additionalResult = scc.get().additionalSynchronizeOperations(content, additionalParams, getLogger(transientVars)); 145 } 146 147 result.aggregateResult(additionalResult); 148 } 149 150 return result; 151 } 152 153 /** 154 * Determine if the content is being imported or synchronized 155 * @param transientVars the parameters from the call. 156 * @return <code>true</code> if the content is being imported, <code>false</code> otherwise 157 * @throws WorkflowException if the object model or the request is not present 158 */ 159 protected boolean isImportMode(Map transientVars) throws WorkflowException 160 { 161 Map<String, Object> parameters = getContextParameters(transientVars); 162 Boolean importContent = (Boolean) parameters.get(IMPORT); 163 return Boolean.TRUE.equals(importContent); 164 } 165 166 /** 167 * Retrieve the scc associated with the workflow. 168 * @param transientVars the parameters from the call. 169 * @return the scc. 170 */ 171 protected Optional<SynchronizableContentsCollection> getSynchronizableContentsCollection(Map transientVars) 172 { 173 return Optional.of(transientVars) 174 .map(vars -> vars.get(SCC_KEY)) 175 .filter(SynchronizableContentsCollection.class::isInstance) 176 .map(SynchronizableContentsCollection.class::cast); 177 } 178 179 /** 180 * Retrieve the scc logger 181 * @param transientVars the parameters from the call. 182 * @return the scc logger 183 */ 184 protected Logger getLogger(Map transientVars) 185 { 186 return Optional.of(transientVars) 187 .map(vars -> vars.get(SCC_LOGGER_KEY)) 188 .filter(Logger.class::isInstance) 189 .map(Logger.class::cast) 190 .orElseGet(() -> new AvalonLoggerAdapter(_logger)); 191 } 192 193 /** 194 * Retrieve the synchronization additional parameters 195 * @param transientVars the parameters from the call. 196 * @return the synchronization additional parameters 197 */ 198 protected Map<String, Object> getAdditionalParameters(Map transientVars) 199 { 200 return Optional.of(transientVars) 201 .map(vars -> vars.get(ADDITIONAL_PARAMS_KEY)) 202 .filter(Map.class::isInstance) 203 .map(Map.class::cast) 204 .orElseGet(HashMap::new); 205 } 206 207 @Override 208 protected int getInvertEditActionId(Map transientVars, Content referencedContent) 209 { 210 int synchroInvertActionId = (int) transientVars.getOrDefault(SYNCHRO_INVERT_EDIT_ACTION_ID_KEY, DEFAULT_SYNCHRO_INVERT_EDIT_ACTION_ID); 211 int invertActionId = (int) transientVars.getOrDefault(INVERT_EDIT_ACTION_ID_KEY, DEFAULT_INVERT_EDIT_ACTION_ID); 212 213 @SuppressWarnings("unchecked") 214 Set<String> notSynchronizedContents = (Set<String>) transientVars.getOrDefault(NOT_SYNCHRONIZED_RELATED_CONTENT_IDS_KEY, Set.of()); 215 return notSynchronizedContents.contains(referencedContent.getId()) ? invertActionId : synchroInvertActionId; 216 } 217 218 @Override 219 protected void notifyContentModifying(Content content, Map<String, Object> values, Map transientVars) throws WorkflowException 220 { 221 // Do nothing 222 } 223 224 @Override 225 protected void updateCommonMetadata(ModifiableContent content, UserIdentity user, SynchronizationResult synchronizationResult) throws WorkflowException 226 { 227 if (synchronizationResult.hasChanged()) 228 { 229 super.updateCommonMetadata(content, user, synchronizationResult); 230 } 231 } 232 233 @Override 234 protected void extractOutgoingReferences(ModifiableContent content, SynchronizationResult synchronizationResult) 235 { 236 if (synchronizationResult.hasChanged()) 237 { 238 super.extractOutgoingReferences(content, synchronizationResult); 239 } 240 } 241 242 @Override 243 protected void prepareOrNotifyContentModified(Content content, Map transientVars, Map args, SynchronizationResult synchronizationResult) throws WorkflowException 244 { 245 if (synchronizationResult.hasChanged()) 246 { 247 boolean notify = Boolean.parseBoolean((String) args.getOrDefault(KEY_NOTIFY_ARGUMENTS, "true")); 248 if (notify) 249 { 250 Map<String, Object> eventParams = new HashMap<>(); 251 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 252 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 253 254 _observationManager.notify(new Event(org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, getUser(transientVars), eventParams)); 255 } 256 else // if notify is false, just prepare the event to send. A post notifyFunction will notify this event... 257 { 258 transientVars.put(AbstractContentFunction.EVENT_TO_NOTIFY_KEY, org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED); 259 } 260 } 261 } 262 263 @Override 264 protected boolean _canWriteModelItem(ModelItem modelItem, Content content, Map transientVars) 265 { 266 if (getSynchronizableContentsCollection(transientVars) 267 .map(SynchronizableContentsCollection::ignoreRestrictions) 268 .orElse(false)) 269 { 270 return true; 271 } 272 return super._canWriteModelItem(modelItem, content, transientVars); 273 } 274 275 @Override 276 public I18nizableText getLabel() 277 { 278 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_EDIT_CONTENT_FUNCTION_LABEL"); 279 } 280}