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        
186        content.setLastValidationDate(validationDate);
187        if (content.getFirstValidationDate() == null)
188        {
189            content.setFirstValidationDate(validationDate);
190        }
191        
192        // Remove the proposal date.
193        content.setProposalDate(null);
194        
195        content.saveChanges();
196    }
197    
198    /**
199     * Validate the referenced contents recursively
200     * @param content The validated content
201     * @param transientVars The parameters from the call.
202     * @throws WorkflowException if an error occurs
203     */
204    protected void _validateRecursively (WorkflowAwareContent content, Map transientVars) throws WorkflowException
205    {
206        if (content instanceof ProgramItem)
207        {
208            // Validate the structure recursively
209            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content);
210            for (ProgramItem child : children)
211            {
212                WorkflowAwareContent waChild = (WorkflowAwareContent) child;
213                if (!_odfWorkflowHelper.isInValidatedStep(waChild))
214                {
215                    _doValidateWorkflowAction((WorkflowAwareContent) child, transientVars, GLOBAL_VALIDATE_ACTION_ID);
216                }
217                else
218                {
219                    // The program item is already validated. Continue to its own children.
220                    _validateRecursively(waChild, transientVars);
221                }
222            }
223        }
224        
225        // Validate others referenced contents
226        if (content instanceof AbstractProgram)
227        {
228            _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), transientVars);
229            _validateReferencedContents(((AbstractProgram) content).getContacts(), transientVars);
230            
231            Set<String> persons = new HashSet<>();
232            Map<String, String[]> personsInChargeByRole = ((AbstractProgram) content).getPersonsInCharge();
233            for (String role : personsInChargeByRole.keySet())
234            {
235                for (String person : personsInChargeByRole.get(role))
236                {
237                    persons.add(person);
238                }
239            }
240            _validateReferencedContents(persons, transientVars);
241        }
242        else if (content instanceof Course)
243        {
244            _validateReferencedContents(((Course) content).getOrgUnits(), transientVars);
245            _validateReferencedContents(((Course) content).getContacts(), transientVars);
246            _validateReferencedContents(((Course) content).getPersonsInCharge(), transientVars);
247        }
248        else if (content instanceof OrgUnit)
249        {
250            _validateReferencedContents(((OrgUnit) content).getContacts(), transientVars);
251        }
252    }
253    
254    /**
255     * Validate the list of contents
256     * @param contentIds The id of contents to validate
257     * @param transientVars The parameters from the call.
258     * @throws WorkflowException if an error occurred
259     */
260    protected void _validateReferencedContents (Collection<String> contentIds, Map transientVars) throws WorkflowException
261    {
262        for (String id : contentIds)
263        {
264            try
265            {
266                if (StringUtils.isNotEmpty(id))
267                {
268                    WorkflowAwareContent content = _resolver.resolveById(id);
269                    if (!_odfWorkflowHelper.isInValidatedStep(content))
270                    {
271                        _doValidateWorkflowAction (content, transientVars, VALIDATE_ACTION_ID);
272                    }
273                }
274            }
275            catch (UnknownAmetysObjectException e)
276            {
277                // Nothing
278            }
279        }
280    }
281    
282    /**
283     * Validate a content
284     * @param content The content to validate
285     * @param transientVars The parameters from the call.
286     * @param actionId The id of validate action
287     * @throws WorkflowException if an error occurred
288     */
289    @SuppressWarnings("unchecked")
290    protected void _doValidateWorkflowAction (WorkflowAwareContent content, Map transientVars, int actionId) throws WorkflowException
291    {
292        try
293        {
294            Map<String, Object> result = _contentWorkflowHelper.doAction(content, actionId, _getInputs(content, transientVars));
295            if (result.containsKey(INVALIDATED_CONTENTS_KEY))
296            {
297                _getInvalidatedContents(transientVars).addAll((Set<String>) result.get(INVALIDATED_CONTENTS_KEY));
298            }
299        }
300        catch (InvalidActionException e)
301        {
302            _getInvalidatedContents(transientVars).add(content.getTitle());
303            _logger.warn("Unable to validate content " + content.getTitle() + " (" + content.getName() + ") : mandatory metadata are probably missing");
304        }
305    }
306    
307    /**
308     * Get the contents that were not validated during global validation
309     * @param transientVars The parameters from the call.
310     * @return The unvalidated contents
311     * @throws WorkflowException if failed to get invalidated contents
312     */
313    @SuppressWarnings("unchecked")
314    protected Set<String> _getInvalidatedContents(Map transientVars) throws WorkflowException
315    {
316        if (!getResultsMap(transientVars).containsKey(INVALIDATED_CONTENTS_KEY))
317        {
318            getResultsMap(transientVars).put(INVALIDATED_CONTENTS_KEY, new HashSet<String>());
319        }
320        
321        return (Set<String>) getResultsMap(transientVars).get(INVALIDATED_CONTENTS_KEY);
322    }
323    
324    /**
325     * Get inputs
326     * @param content The content to validate
327     * @param transientVars the parameters from the call.
328     * @return The inputs
329     */
330    protected Map<String, Object> _getInputs (Content content, Map transientVars)
331    {
332        Map<String, Object> inputs = new HashMap<>(); 
333        
334        // Provide the redirector
335        Redirector redirector = (Redirector) transientVars.get(Redirector.class.getName());
336        inputs.put(Redirector.class.getName(), redirector);
337        
338        // Provide the content key 
339        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 
340        
341        // Provide a map for providing data to the generator 
342        Map<String, Object> result = new HashMap<>(); 
343        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, result); 
344        
345        return inputs;
346    }
347    
348}