001/*
002 *  Copyright 2012 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.odf.workflow;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.environment.Redirector;
033import org.apache.commons.lang3.StringUtils;
034
035import org.ametys.cms.ObservationConstants;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
038import org.ametys.cms.repository.WorkflowAwareContent;
039import org.ametys.cms.workflow.AbstractContentFunction;
040import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
041import org.ametys.cms.workflow.ContentWorkflowHelper;
042import org.ametys.core.observation.Event;
043import org.ametys.odf.ODFHelper;
044import org.ametys.odf.ProgramItem;
045import org.ametys.odf.course.Course;
046import org.ametys.odf.orgunit.OrgUnit;
047import org.ametys.odf.program.AbstractProgram;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.AmetysRepositoryException;
050import org.ametys.plugins.repository.UnknownAmetysObjectException;
051import org.ametys.plugins.repository.version.VersionableAmetysObject;
052import org.ametys.plugins.workflow.AbstractWorkflowComponent;
053import org.ametys.runtime.i18n.I18nizableText;
054
055import com.opensymphony.module.propertyset.PropertySet;
056import com.opensymphony.workflow.InvalidActionException;
057import com.opensymphony.workflow.WorkflowException;
058
059/**
060 * OSWorkflow function for validating a ODF content.
061 * If argument "recursively" is used, the referenced contents will be validated too.
062 */
063public class ValidateODFContentFunction extends AbstractContentFunction
064{
065    /** Label for the validated version of contents. */
066    public static final String VALID_LABEL = "Live";
067    
068    /** Label for the validated version of contents. */
069    public static final String VALIDATE_RECURSIVELY_ARG = "recursively";
070    
071    /** Constant for storing the result map into the transient variables map. */
072    public static final String INVALIDATED_CONTENTS_KEY = ValidateODFContentFunction.class.getName() + "-invalidatedContents";
073    
074    /** The action id of global validation */
075    public static final int GLOBAL_VALIDATE_ACTION_ID = 900;
076    
077    /** The action id of global validation */
078    public static final int VALIDATE_ACTION_ID = 4;
079    
080    /** The validate step id */
081    public static final int VALIDATED_STEP_ID = 3;
082
083    /** The Ametys object resolver */
084    protected AmetysObjectResolver _resolver;
085    /** The workflow helper for contents */
086    protected ContentWorkflowHelper _contentWorkflowHelper;
087    /** The ODF helper */
088    protected ODFHelper _odfHelper;
089    /** The ODF workflow helper */
090    protected ODFWorkflowHelper _odfWorkflowHelper;
091    
092    @Override
093    public void service(ServiceManager smanager) throws ServiceException
094    {
095        super.service(smanager);
096        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
097        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
098        _odfWorkflowHelper = (ODFWorkflowHelper) smanager.lookup(ODFWorkflowHelper.ROLE);
099        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
100    }
101    
102    @Override
103    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
104    {
105        _logger.info("Performing content validation");
106        
107        WorkflowAwareContent content = getContent(transientVars);
108        
109        if (!(content instanceof ModifiableWorkflowAwareContent))
110        {
111            throw new IllegalArgumentException("The provided content " + content.getId() + " is not a ModifiableWorkflowAwareContent.");
112        }
113        
114        ModifiableWorkflowAwareContent modifiableContent = (ModifiableWorkflowAwareContent) content;
115        
116        try
117        {
118            _validateContent(modifiableContent);
119            
120            if (args.containsKey(VALIDATE_RECURSIVELY_ARG))
121            {
122                // Validate recursively the referenced contents
123                _validateRecursively(content, transientVars);
124            }
125            
126            // Set the current step ID.
127            _setCurrentStepIdAndNotify(modifiableContent, transientVars);
128            // Create a new version.
129            _createVersion(modifiableContent);
130            // Add the valid label on the newly created version.
131            _addLabel(modifiableContent, VALID_LABEL);
132            
133            if (_getInvalidatedContents(transientVars).size() > 0)
134            {
135                String invalidatedContents = StringUtils.join(_getInvalidatedContents(transientVars), ",");
136                addWorkflowWarning(transientVars, new I18nizableText("plugin.odf", "PLUGINS_ODF_WORKFLOW_ACTION_GLOBAL_VALIDATION_INVALIDATED", Collections.singletonList(invalidatedContents)));
137            }
138        }
139        catch (RepositoryException e)
140        {
141            throw new WorkflowException("Unable to link the workflow to the content", e);
142        }
143        catch (AmetysRepositoryException e)
144        {
145            throw new WorkflowException("Unable to validate the content", e);
146        }
147        
148        _notifyObservers(transientVars, content);
149    }
150    
151    /**
152     * Notify observers of content validation
153     * @param transientVars The transient variables
154     * @param content The created content
155     * @throws AmetysRepositoryException If an error occurred with the repository
156     * @throws WorkflowException If an error occurred with the workflow
157     */
158    protected void _notifyObservers (Map transientVars, Content content) throws AmetysRepositoryException, WorkflowException
159    {
160        Map<String, Object> eventParams = new HashMap<>();
161        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
162        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
163        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_VALIDATED, getUser(transientVars), eventParams));
164    }
165
166    /**
167     * Validates the content.
168     * @param content the content.
169     * @throws WorkflowException if an error occurs.
170     * @throws RepositoryException if an error occurs.
171     */
172    protected void _validateContent(ModifiableWorkflowAwareContent content) throws WorkflowException, RepositoryException
173    {
174        if (!(content instanceof VersionableAmetysObject))
175        {
176            throw new WorkflowException("Invalid content implementation: " + content);
177        }
178        
179        Date validationDate = new Date();
180        boolean isValid = Arrays.asList(((VersionableAmetysObject) content).getAllLabels()).contains(VALID_LABEL);
181        if (!isValid)
182        {
183            content.setLastMajorValidationDate(validationDate);
184        }
185        content.setLastValidationDate(validationDate);
186        // Remove the proposal date.
187        content.setProposalDate(null);
188        
189        content.saveChanges();
190    }
191    
192    /**
193     * Validate the referenced contents recursively
194     * @param content The validated content
195     * @param transientVars The parameters from the call.
196     * @throws WorkflowException if an error occurs
197     */
198    protected void _validateRecursively (WorkflowAwareContent content, Map transientVars) throws WorkflowException
199    {
200        if (content instanceof ProgramItem)
201        {
202            // Validate the structure recursively
203            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content);
204            for (ProgramItem child : children)
205            {
206                WorkflowAwareContent waChild = (WorkflowAwareContent) child;
207                if (!_odfWorkflowHelper.isInValidatedStep(waChild))
208                {
209                    _doValidateWorkflowAction((WorkflowAwareContent) child, transientVars, GLOBAL_VALIDATE_ACTION_ID);
210                }
211                else
212                {
213                    // The program item is already validated. Continue to its own children.
214                    _validateRecursively(waChild, transientVars);
215                }
216            }
217        }
218        
219        // Validate others referenced contents
220        if (content instanceof AbstractProgram)
221        {
222            _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), transientVars);
223            _validateReferencedContents(((AbstractProgram) content).getContacts(), transientVars);
224            
225            Set<String> persons = new HashSet<>();
226            Map<String, String[]> personsInChargeByRole = ((AbstractProgram) content).getPersonsInCharge();
227            for (String role : personsInChargeByRole.keySet())
228            {
229                for (String person : personsInChargeByRole.get(role))
230                {
231                    persons.add(person);
232                }
233            }
234            _validateReferencedContents(persons, transientVars);
235        }
236        else if (content instanceof Course)
237        {
238            _validateReferencedContents(((Course) content).getOrgUnits(), transientVars);
239            _validateReferencedContents(((Course) content).getContacts(), transientVars);
240            _validateReferencedContents(((Course) content).getPersonsInCharge(), transientVars);
241        }
242        else if (content instanceof OrgUnit)
243        {
244            _validateReferencedContents(((OrgUnit) content).getContacts(), transientVars);
245        }
246    }
247    
248    /**
249     * Validate the list of contents
250     * @param contentIds The id of contents to validate
251     * @param transientVars The parameters from the call.
252     * @throws WorkflowException if an error occurred
253     */
254    protected void _validateReferencedContents (Collection<String> contentIds, Map transientVars) throws WorkflowException
255    {
256        for (String id : contentIds)
257        {
258            try
259            {
260                if (StringUtils.isNotEmpty(id))
261                {
262                    WorkflowAwareContent content = _resolver.resolveById(id);
263                    if (!_odfWorkflowHelper.isInValidatedStep(content))
264                    {
265                        _doValidateWorkflowAction (content, transientVars, VALIDATE_ACTION_ID);
266                    }
267                }
268            }
269            catch (UnknownAmetysObjectException e)
270            {
271                // Nothing
272            }
273        }
274    }
275    
276    /**
277     * Validate a content
278     * @param content The content to validate
279     * @param transientVars The parameters from the call.
280     * @param actionId The id of validate action
281     * @throws WorkflowException if an error occurred
282     */
283    @SuppressWarnings("unchecked")
284    protected void _doValidateWorkflowAction (WorkflowAwareContent content, Map transientVars, int actionId) throws WorkflowException
285    {
286        try
287        {
288            Map<String, Object> result = _contentWorkflowHelper.doAction(content, actionId, _getInputs(content, transientVars));
289            if (result.containsKey(INVALIDATED_CONTENTS_KEY))
290            {
291                _getInvalidatedContents(transientVars).addAll((Set<String>) result.get(INVALIDATED_CONTENTS_KEY));
292            }
293        }
294        catch (InvalidActionException e)
295        {
296            _getInvalidatedContents(transientVars).add(content.getTitle());
297            _logger.warn("Unable to validate content " + content.getTitle() + " (" + content.getName() + ") : mandatory metadata are probably missing");
298        }
299    }
300    
301    /**
302     * Get the contents that were not validated during global validation
303     * @param transientVars The parameters from the call.
304     * @return The unvalidated contents
305     * @throws WorkflowException if failed to get invalidated contents
306     */
307    @SuppressWarnings("unchecked")
308    protected Set<String> _getInvalidatedContents(Map transientVars) throws WorkflowException
309    {
310        if (!getResultsMap(transientVars).containsKey(INVALIDATED_CONTENTS_KEY))
311        {
312            getResultsMap(transientVars).put(INVALIDATED_CONTENTS_KEY, new HashSet<String>());
313        }
314        
315        return (Set<String>) getResultsMap(transientVars).get(INVALIDATED_CONTENTS_KEY);
316    }
317    
318    /**
319     * Get inputs
320     * @param content The content to validate
321     * @param transientVars the parameters from the call.
322     * @return The inputs
323     */
324    protected Map<String, Object> _getInputs (Content content, Map transientVars)
325    {
326        Map<String, Object> inputs = new HashMap<>(); 
327        
328        // Provide the redirector
329        Redirector redirector = (Redirector) transientVars.get(Redirector.class.getName());
330        inputs.put(Redirector.class.getName(), redirector);
331        
332        // Provide the content key 
333        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 
334        
335        // Provide a map for providing data to the generator 
336        Map<String, Object> result = new HashMap<>(); 
337        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, result); 
338        
339        return inputs;
340    }
341    
342}