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