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