001/* 002 * Copyright 2020 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.time.ZonedDateTime; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.commons.lang3.StringUtils; 029 030import org.ametys.cms.content.compare.ContentComparator; 031import org.ametys.cms.content.compare.ContentComparatorChange; 032import org.ametys.cms.data.ContentValue; 033import org.ametys.cms.data.type.ModelItemTypeConstants; 034import org.ametys.cms.repository.Content; 035import org.ametys.cms.repository.ModifiableWorkflowAwareContent; 036import org.ametys.cms.repository.WorkflowAwareContent; 037import org.ametys.cms.workflow.history.VersionInformation; 038import org.ametys.core.user.UserIdentity; 039import org.ametys.core.util.I18nUtils; 040import org.ametys.plugins.repository.AmetysObjectResolver; 041import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 042import org.ametys.plugins.repository.lock.LockHelper; 043import org.ametys.plugins.repository.lock.LockableAmetysObject; 044import org.ametys.plugins.repository.model.RepositoryDataContext; 045import org.ametys.plugins.repository.version.VersionableAmetysObject; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.i18n.I18nizableTextParameter; 048import org.ametys.runtime.model.ElementDefinition; 049import org.ametys.runtime.model.ModelHelper; 050import org.ametys.runtime.model.View; 051import org.ametys.runtime.model.ViewItemContainer; 052import org.ametys.runtime.model.type.DataContext; 053 054import com.opensymphony.module.propertyset.PropertySet; 055import com.opensymphony.workflow.WorkflowException; 056 057/** 058 * OSWorkflow function to restore an old revision of a content. 059 * Builds a Map with the old content's attributes values, and passes it to the 060 * {@link EditContentFunction}, which does the real job. 061 */ 062public class RestoreRevisionFunction extends AbstractContentFunction 063{ 064 private AmetysObjectResolver _resolver; 065 private ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 066 private ContentComparator _contentComparator; 067 private I18nUtils _i18nUtils; 068 069 @Override 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 super.service(manager); 073 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 074 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 075 _contentComparator = (ContentComparator) manager.lookup(ContentComparator.ROLE); 076 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 077 } 078 079 @Override 080 public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException 081 { 082 WorkflowAwareContent content = getContent(transientVars); 083 UserIdentity user = getUser(transientVars); 084 085 if (!(content instanceof ModifiableWorkflowAwareContent)) 086 { 087 throw new IllegalArgumentException("The provided content " + content.getId() + " is not a ModifiableWorkflowAwareContent."); 088 } 089 090 ModifiableWorkflowAwareContent modifiableContent = (ModifiableWorkflowAwareContent) content; 091 092 if (content instanceof LockableAmetysObject) 093 { 094 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 095 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 096 { 097 throw new WorkflowException ("The user '" + user + "' try to restore the content '" + content.getId() + "', but this content is locked by the user '" + user + "'"); 098 } 099 else if (lockableContent.isLocked()) 100 { 101 lockableContent.unlock(); 102 } 103 } 104 105 String contentVersion = (String) getContextParameters(transientVars).get("contentVersion"); 106 107 Content oldContent = _resolver.resolveById(content.getId()); 108 if (oldContent instanceof VersionableAmetysObject versionAware) 109 { 110 versionAware.switchToRevision(contentVersion); 111 } 112 113 Map<String, Object> results = getResultsMap(transientVars); 114 Map<String, Object> brokenReferences = new HashMap<>(); 115 results.put("brokenReferences", brokenReferences); 116 117 Map<String, Object> parameters = getContextParameters(transientVars); 118 Set<String> externalizableData = _externalizableDataProviderEP.getExternalizableDataPaths(content); 119 120 // Get values from version to restore 121 DataContext context = RepositoryDataContext.newInstance() 122 .withExternalizableData(externalizableData) 123 .withEmptyValues(true); 124 Map<String, Object> newValues = oldContent.dataToMap(context); 125 126 String[] changingAttributes; 127 // CMS-11242 Only keep values with differences 128 try 129 { 130 changingAttributes = _contentComparator.compare(content, oldContent) 131 .getChanges() 132 .stream() 133 .map(ContentComparatorChange::getAttributeDataPath) 134 .map(ModelHelper::getDefinitionPathFromDataPath) 135 .distinct() 136 .toArray(String[]::new); 137 } 138 catch (IOException e) 139 { 140 throw new WorkflowException("An exception occured during content version comparison", e); 141 } 142 143 String language = oldContent.getLanguage(); 144 145 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 146 i18nParams.put("versionNumber", new I18nizableText(VersionInformation.getVersionNameFromRawVersion(contentVersion))); 147 String versionNumberComment = _i18nUtils.translate(new I18nizableText("plugin.cms", "CONTENT_RESTORE_COMMENT_VERSION_NUMBER", i18nParams), language); 148 149 StringBuilder sb = new StringBuilder(); 150 sb.append(versionNumberComment); 151 152 String comment = (String) transientVars.get("comment"); 153 if (StringUtils.isNotEmpty(comment)) 154 { 155 sb.append("\n"); 156 sb.append(comment); 157 } 158 159 transientVars.put("comment", sb.toString()); 160 161 View view = View.of(content.getModel(), changingAttributes); 162 163 // Exclude invalid links 164 newValues = _processContents(view, newValues, "", brokenReferences); 165 166 parameters.put(EditContentFunction.VIEW, view); 167 parameters.put(EditContentFunction.VALUES_KEY, newValues); 168 parameters.put(EditContentFunction.GLOBAL_VALIDATION, false); 169 parameters.put(EditContentFunction.QUIT, Boolean.TRUE); 170 171 modifiableContent.setLastContributor(user); 172 modifiableContent.setLastModified(ZonedDateTime.now()); 173 174 // Remove the proposal date. 175 modifiableContent.setProposalDate(null); 176 177 // Commit changes 178 modifiableContent.saveChanges(); 179 } 180 181 private ContentValue _processContentValue(ContentValue value, String dataPath, ElementDefinition definition, Map<String, Object> brokenReferences) 182 { 183 if (_resolver.hasAmetysObjectForId(value.getContentId())) 184 { 185 return value; 186 } 187 else if (!brokenReferences.containsKey(dataPath)) 188 { 189 brokenReferences.put(dataPath, definition.getLabel()); 190 } 191 192 return null; 193 } 194 195 @SuppressWarnings("unchecked") 196 private Map<String, Object> _processContents(ViewItemContainer viewItemContainer, Map<String, Object> values, String dataPath, Map<String, Object> brokenReferences) 197 { 198 if (values == null) 199 { 200 return null; 201 } 202 203 Map<String, Object> result = new HashMap<>(); 204 205 org.ametys.plugins.repository.model.ViewHelper.visitView(viewItemContainer, 206 (element, definition) -> { 207 // simple element 208 String name = definition.getName(); 209 Object value = values.get(name); 210 211 if (values.containsKey(name)) 212 { 213 if (definition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)) 214 { 215 Object newValue = null; 216 if (value instanceof ContentValue[]) 217 { 218 List<ContentValue> validContents = new ArrayList<>(); 219 220 for (ContentValue contentValue : (ContentValue[]) value) 221 { 222 ContentValue newContentValue = _processContentValue(contentValue, dataPath + name, definition, brokenReferences); 223 if (newContentValue != null) 224 { 225 validContents.add(newContentValue); 226 } 227 } 228 229 newValue = validContents; 230 } 231 else if (value instanceof ContentValue) 232 { 233 ContentValue newContentValue = _processContentValue((ContentValue) value, dataPath + name, definition, brokenReferences); 234 newValue = newContentValue; 235 } 236 237 result.put(name, newValue); 238 } 239 else 240 { 241 result.put(name, value); 242 } 243 } 244 }, 245 (group, definition) -> { 246 // composite 247 String name = definition.getName(); 248 if (values.containsKey(name)) 249 { 250 result.put(name, _processContents(group, (Map<String, Object>) values.get(name), dataPath + name + "/" , brokenReferences)); 251 } 252 }, 253 (group, definition) -> { 254 // repeater 255 String name = definition.getName(); 256 if (values.containsKey(name)) 257 { 258 List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name); 259 260 List<Map<String, Object>> newEntries = new ArrayList<>(); 261 for (int i = 0; i < entries.size(); i++) 262 { 263 newEntries.add(_processContents(group, entries.get(i), dataPath + name + "[" + (i + 1) + "]/" , brokenReferences)); 264 } 265 266 result.put(name, newEntries); 267 } 268 }, 269 group -> result.putAll(_processContents(group, values, dataPath, brokenReferences))); 270 271 return result; 272 } 273 274 @Override 275 public FunctionType getFunctionExecType() 276 { 277 return FunctionType.PRE; 278 } 279 280 @Override 281 public I18nizableText getLabel() 282 { 283 return new I18nizableText("plugin.cms", "PLUGINS_CMS_RESTORE_REVISION_FUNCTION_LABEL"); 284 } 285}