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