001/*
002 *  Copyright 2011 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.util.Date;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.concurrent.Future;
023
024import javax.jcr.Node;
025import javax.jcr.RepositoryException;
026import javax.jcr.Session;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.lang3.StringUtils;
031
032import org.ametys.cms.FilterNameHelper;
033import org.ametys.cms.ObservationConstants;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.repository.ModifiableContent;
036import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
037import org.ametys.core.observation.Event;
038import org.ametys.core.observation.ObservationManager;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.population.UserPopulationDAO;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.AmetysRepositoryException;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.repository.RepositoryConstants;
045import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
046import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
047import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore;
048import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore;
049
050import com.opensymphony.module.propertyset.PropertySet;
051import com.opensymphony.workflow.FunctionProvider;
052import com.opensymphony.workflow.WorkflowException;
053import com.opensymphony.workflow.spi.WorkflowEntry;
054import com.opensymphony.workflow.spi.WorkflowStore;
055
056/**
057 * OSWorkflow function for creating a content.
058 */
059public class CreateContentFunction extends AbstractContentWorkflowComponent implements FunctionProvider
060{
061    /** Constant for storing the content name to use into the transient variables map. */
062    public static final String CONTENT_NAME_KEY = CreateContentFunction.class.getName() + "$contentName";
063    /** Constant for storing the content title to use into the transient variables map. */
064    public static final String CONTENT_TITLE_KEY = CreateContentFunction.class.getName() + "$contentTitle";
065    /** Constant for storing the content type to use into the transient variables map. */
066    public static final String CONTENT_TYPES_KEY = CreateContentFunction.class.getName() + "$contentTypes";
067    /** Constant for storing the content type to use into the transient variables map. */
068    public static final String CONTENT_MIXINS_KEY = CreateContentFunction.class.getName() + "$mixins";
069    /** Constant for storing the content language to use into the transient variables map. */
070    public static final String CONTENT_LANGUAGE_KEY = CreateContentFunction.class.getName() + "$contentLanguage";
071    /** Constant for storing the content language to use into the transient variables map. */
072    public static final String PARENT_CONTENT_ID_KEY = CreateContentFunction.class.getName() + "$parentContentId";
073    /** Constant for storing the content language to use into the transient variables map. */
074    public static final String PARENT_CONTENT_METADATA_PATH_KEY = CreateContentFunction.class.getName() + "$parentContentMetadataPath";
075    
076    /** Ametys object resolver available to subclasses. */
077    protected AmetysObjectResolver _resolver;
078    /** Observation manager available to subclasses. */
079    protected ObservationManager _observationManager;
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        super.service(manager);
085        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
086        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
087    }
088    
089    @Override
090    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
091    {
092        _logger.info("Performing content creation");
093        
094        try
095        {
096            String desiredContentName = _getNonNullVar(transientVars, CONTENT_NAME_KEY, "Missing content name");
097            String contentTitle = _getNonNullVar(transientVars, CONTENT_TITLE_KEY, "Missing content title");
098            
099            String[] contentTypes = null;
100            
101            if (transientVars.get(CONTENT_TYPES_KEY) != null)
102            {
103                contentTypes = (String[]) transientVars.get(CONTENT_TYPES_KEY);
104            }
105            else
106            {
107                throw new WorkflowException("Missing contents types");
108            }
109            
110            String[] mixins = new String[0];
111            if (transientVars.get(CONTENT_MIXINS_KEY) != null)
112            {
113                mixins = (String[]) transientVars.get(CONTENT_MIXINS_KEY);
114            }
115            
116            String contentLanguage = _getNonNullVar(transientVars, CONTENT_LANGUAGE_KEY, "Missing content language");
117            
118            ModifiableTraversableAmetysObject contents = _getContentRoot(transientVars);
119            
120            ModifiableWorkflowAwareContent content = _createContent(transientVars, desiredContentName, contents);
121
122            content.setTitle(contentTitle);
123            content.setTypes(contentTypes);
124            content.setMixinTypes(mixins);
125            content.setLanguage(contentLanguage);
126            
127            // Set the workflow id
128            long workflowId = ((WorkflowEntry) transientVars.get("entry")).getId();
129            content.setWorkflowId(workflowId);
130            
131            _populateContent(transientVars, content);
132            
133            // FIXME previous statements may have failed.
134            contents.saveChanges();
135            
136            _populateAdditionalData(transientVars, content);
137            
138            Node node = content.getNode();
139            Session session = node.getSession();
140            
141            try
142            {
143                WorkflowStore workflowStore = (WorkflowStore) transientVars.get("store");
144                
145                if (workflowStore instanceof AmetysObjectWorkflowStore)
146                {
147                    AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore;
148                    ametysObjectWorkflowStore.bindAmetysObject(content);
149                }
150                
151                if (workflowStore instanceof AbstractJackrabbitWorkflowStore)
152                {
153                    AbstractJackrabbitWorkflowStore jackrabbitWorkflowStore = (AbstractJackrabbitWorkflowStore) workflowStore;
154                    Node workflowEntryNode = jackrabbitWorkflowStore.getEntryNode(session, workflowId);
155                    
156                    Integer actionId = (Integer) transientVars.get("actionId");
157                    if (actionId != null)
158                    {
159                        workflowEntryNode.setProperty("ametys-internal:initialActionId", actionId);
160                    }
161                }
162            }
163            catch (RepositoryException e)
164            {
165                throw new AmetysRepositoryException("Unable to link the workflow to the content", e);
166            }
167            
168            String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY);
169            String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY);
170            if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath))
171            {
172                node.setProperty("ametys-internal:subContent", true);
173            }
174            
175            session.save();
176            
177            // Notify observers
178            _notifyContentAdded(content, transientVars);
179            
180            // Content created
181            transientVars.put(CONTENT_KEY, content);
182            
183            getResultsMap(transientVars).put("contentId", content.getId());
184            getResultsMap(transientVars).put(CONTENT_KEY, content);
185        }
186        catch (RepositoryException e)
187        {
188            throw new WorkflowException("Unable to link the workflow to the content", e);
189        }
190        catch (AmetysRepositoryException e)
191        {
192            throw new WorkflowException("Unable to create the content", e);
193        }
194    }
195
196    /**
197     * Notify observers that the content has been created
198     * @param content The content added
199     * @param transientVars The workflow vars
200     * @return The {@link Future} objects of the asynchronous observers
201     * @throws WorkflowException If an error occurred
202     */
203    protected List<Future> _notifyContentAdded(Content content, Map transientVars) throws WorkflowException
204    {
205        Map<String, Object> eventParams = new HashMap<>();
206        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
207        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
208
209        return _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, getUser(transientVars), eventParams));
210    }
211
212    /**
213     * Create the content object.
214     * @param transientVars the workflow parameters.
215     * @param desiredContentName the desired content name.
216     * @param contentsNode the content root node in the repository.
217     * @return the created Content.
218     */
219    protected ModifiableWorkflowAwareContent _createContent(Map transientVars, String desiredContentName, ModifiableTraversableAmetysObject contentsNode)
220    {
221        ModifiableWorkflowAwareContent content = null;
222        
223        String contentName = FilterNameHelper.filterName(desiredContentName);
224        int errorCount = 0;
225        do
226        {
227            if (errorCount != 0)
228            {
229                contentName = FilterNameHelper.filterName(desiredContentName + " " + (errorCount + 1));
230            }
231            try
232            {
233                String type = _getObjectType(transientVars);
234                content = contentsNode.createChild(contentName, type);
235            }
236            catch (RepositoryIntegrityViolationException e)
237            {
238                // Content name is already used
239                errorCount++;
240            }
241        }
242        while (content == null);
243        
244        return content;
245    }
246    
247    /**
248     * Return the type of the object to be created.
249     * Ex: ametys:defaultContent.
250     * @param transientVars The workflow vars
251     * @return The type of the object to be used during content creation.
252     */
253    protected String _getObjectType(Map transientVars)
254    {
255        return RepositoryConstants.NAMESPACE_PREFIX + ":defaultContent";
256    }
257
258    /**
259     * Retrieve the content root.
260     * @param transientVars the workflow parameters.
261     * @return the content root node.
262     * @throws WorkflowException if an error occurs
263     */
264    protected ModifiableTraversableAmetysObject _getContentRoot(Map transientVars) throws WorkflowException
265    {
266        String parentContentId = (String) transientVars.get(PARENT_CONTENT_ID_KEY);
267        String parentContentMetadataPath = (String) transientVars.get(PARENT_CONTENT_METADATA_PATH_KEY);
268        
269        if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentContentMetadataPath))
270        {
271            return _getSubContentRoot(parentContentId, parentContentMetadataPath);
272        }
273        
274        return _resolver.resolveByPath("/" + RepositoryConstants.NAMESPACE_PREFIX + ":contents");
275    }
276    
277    /**
278     * Get the content root when creating a sub-content.
279     * @param parentContentId the parent content ID.
280     * @param parentContentMetadataPath the path of the metadata in which to create the sub-content.
281     * @return the content collection metadata.
282     * @throws WorkflowException if an error occurs.
283     */
284    protected ModifiableTraversableAmetysObject _getSubContentRoot(String parentContentId, String parentContentMetadataPath) throws WorkflowException
285    {
286        Content parentContent = _resolver.resolveById(parentContentId);
287        
288        if (parentContent instanceof ModifiableContent)
289        {
290            ModifiableCompositeMetadata meta = ((ModifiableContent) parentContent).getMetadataHolder();
291            
292            String metaPath = parentContentMetadataPath.replace('/', '.');
293            
294            String[] metadatas = StringUtils.split(metaPath, '.');
295            for (int i = 0; i < metadatas.length - 1; i++)
296            {
297                meta = meta.getCompositeMetadata(metadatas[i], true);
298            }
299            
300            String metaName = metadatas[metadatas.length - 1];
301            
302            return meta.getObjectCollection(metaName, true);
303        }
304        else
305        {
306            throw new WorkflowException("The content " + parentContentId + " is not modifiable.");
307        }
308    }
309    
310    /**
311     * Get a workflow parameter, throwing an exception if it is null.
312     * @param transientVars the workflow parameters.
313     * @param name the variable name.
314     * @param exceptionName label of the exception to throw if the variable is null.
315     * @return the variable value.
316     * @throws WorkflowException If an error occurred
317     */
318    protected String _getNonNullVar(Map transientVars, String name, String exceptionName) throws WorkflowException
319    {
320        String value = (String) transientVars.get(name);
321        if (value == null)
322        {
323            throw new WorkflowException(exceptionName);
324        }
325        return value;
326    }
327    
328    /**
329     * Populate the content.
330     * @param transientVars the transient variables.
331     * @param content the content.
332     * @throws WorkflowException if an error occurs.
333     */
334    protected void _populateContent(Map transientVars, ModifiableContent content) throws WorkflowException
335    {
336        UserIdentity user = getUser(transientVars);
337        if (user == null)
338        {
339            // FIXME Login can be null when creating the content in the background environment.
340            user = getSystemUser();
341        }
342        
343        // Add standard metadata
344        content.setCreator(user);
345        content.setCreationDate(new Date());
346        content.setLastContributor(user);
347        content.setLastModified(new Date());
348    }
349    
350    /**
351     * Get the user to use when the content is created in a background environment.
352     * @return The anonymous user
353     */
354    protected UserIdentity getSystemUser ()
355    {
356        return UserPopulationDAO.SYSTEM_USER_IDENTITY;
357    }
358    
359    /**
360     * Populate the content.
361     * @param transientVars the transient variables.
362     * @param content the content.
363     * @throws WorkflowException if an error occurs.
364     */
365    protected void _populateAdditionalData(Map transientVars, ModifiableContent content) throws WorkflowException
366    {
367        // Nothing to do.
368    }
369}