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