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