001/*
002 *  Copyright 2010 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.content;
017
018import java.io.IOException;
019import java.text.DateFormat;
020import java.text.SimpleDateFormat;
021import java.time.LocalDate;
022import java.time.format.DateTimeFormatter;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.Date;
026import java.util.List;
027
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.generation.Generator;
036import org.apache.cocoon.generation.ServiceableGenerator;
037import org.apache.cocoon.xml.AttributesImpl;
038import org.apache.cocoon.xml.XMLUtils;
039import org.apache.commons.lang.ArrayUtils;
040import org.apache.commons.lang.StringUtils;
041import org.xml.sax.SAXException;
042
043import org.ametys.cms.contenttype.ContentType;
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.contenttype.ContentTypesHelper;
046import org.ametys.cms.contenttype.MetadataManager;
047import org.ametys.cms.contenttype.MetadataSet;
048import org.ametys.cms.languages.Language;
049import org.ametys.cms.languages.LanguagesManager;
050import org.ametys.cms.repository.Content;
051import org.ametys.cms.repository.WorkflowAwareContent;
052import org.ametys.cms.repository.comment.Comment;
053import org.ametys.cms.repository.comment.CommentableContent;
054import org.ametys.core.util.DateUtils;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject;
057import org.ametys.plugins.repository.jcr.JCRAmetysObject;
058import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
059import org.ametys.plugins.workflow.store.AmetysStep;
060import org.ametys.plugins.workflow.support.WorkflowHelper;
061import org.ametys.plugins.workflow.support.WorkflowProvider;
062import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.runtime.parameter.ParameterHelper;
065
066import com.opensymphony.workflow.WorkflowException;
067import com.opensymphony.workflow.spi.Step;
068
069/**
070 * {@link Generator} for rendering raw content data.
071 */
072public class ContentGenerator extends ServiceableGenerator
073{
074    /** The display date format. */
075    protected static final DateFormat _DC_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
076    
077    /** Content type extension point. */
078    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
079    /** Metadata manager. */
080    protected MetadataManager _metadataManager;
081    /** The language manager */
082    protected LanguagesManager _languageManager;
083    /** The workflow provider */
084    protected WorkflowProvider _workflowProvider;
085    /** The workflow helper */
086    protected WorkflowHelper _worklflowHelper;
087    /** Helper for content types */
088    protected ContentTypesHelper _cTypesHelper; 
089    
090    @Override
091    public void service(ServiceManager serviceManager) throws ServiceException
092    {
093        super.service(serviceManager);
094        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
095        _metadataManager = (MetadataManager) serviceManager.lookup(MetadataManager.ROLE);
096        _languageManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE);
097        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
098        _worklflowHelper = (WorkflowHelper) serviceManager.lookup(WorkflowHelper.ROLE);
099        _cTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
100    }
101    
102    public void generate() throws IOException, SAXException, ProcessingException
103    {
104        contentHandler.startDocument();
105        _generateContent();
106        contentHandler.endDocument();
107    }
108    
109    /**
110     * Generate the content (with the start/end document)
111     * @throws SAXException if an error occurs while SAXing
112     * @throws IOException if an error occurs
113     * @throws ProcessingException if an error occurs
114     */
115    protected void _generateContent() throws SAXException, IOException, ProcessingException
116    {
117        Request request = ObjectModelHelper.getRequest(objectModel);
118        Content content = (Content) request.getAttribute(Content.class.getName());
119        
120        // SAX the content
121        _saxContent(content);
122    }
123    
124    /**
125     * SAX the content 
126     * @param content The content to SAX
127     * @throws SAXException if an error occurs while SAXing
128     * @throws IOException if an error occurs
129     * @throws ProcessingException if an error occurs
130     */
131    protected void _saxContent (Content content) throws SAXException, IOException, ProcessingException
132    {
133        AttributesImpl attrs = new AttributesImpl();
134        attrs.addCDATAAttribute("id", content.getId());
135        if (content instanceof JCRAmetysObject)
136        {
137            try
138            {
139                attrs.addCDATAAttribute("uuid", ((JCRAmetysObject) content).getNode().getIdentifier());
140            }
141            catch (RepositoryException e)
142            {
143                throw new ProcessingException("Unable to get jcr UUID for content '" + content.getId() + "'", e);
144            }
145        }
146        attrs.addCDATAAttribute("id", content.getId());
147        attrs.addCDATAAttribute("name", content.getName());
148        attrs.addCDATAAttribute("title", content.getTitle());
149        attrs.addCDATAAttribute("language", content.getLanguage());
150        attrs.addCDATAAttribute("createdAt", ParameterHelper.valueToString(content.getCreationDate()));
151        attrs.addCDATAAttribute("creator", content.getCreator().getLogin());
152        attrs.addCDATAAttribute("lastModifiedAt", ParameterHelper.valueToString(content.getLastModified()));
153        Date lastValidatedAt = content.getLastValidationDate();
154        if (lastValidatedAt != null)
155        {
156            attrs.addCDATAAttribute("lastValidatedAt", ParameterHelper.valueToString(lastValidatedAt));
157        }
158        attrs.addCDATAAttribute("lastContributor", content.getLastContributor().getLogin());
159        attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent));
160        
161        _addAttributeIfNotNull (attrs, "iconGlyph", _cTypesHelper.getIconGlyph(content));
162        _addAttributeIfNotNull (attrs, "iconDecorator", _cTypesHelper.getIconDecorator(content));
163        
164        _addAttributeIfNotNull (attrs, "smallIcon", _cTypesHelper.getSmallIcon(content));
165        _addAttributeIfNotNull (attrs, "mediumIcon", _cTypesHelper.getMediumIcon(content));
166        _addAttributeIfNotNull (attrs, "largeIcon", _cTypesHelper.getLargeIcon(content));
167        
168        XMLUtils.startElement(contentHandler, "content", attrs);
169
170        MetadataSet metadataSet = _getMetadataSet(content);
171        XMLUtils.startElement(contentHandler, "metadata");
172        _saxMetadata(content, metadataSet);
173        XMLUtils.endElement(contentHandler, "metadata");
174        
175        if (metadataSet.isEdition())
176        {
177            XMLUtils.startElement(contentHandler, "comments");
178            _saxMetadataComments(content, metadataSet);
179            XMLUtils.endElement(contentHandler, "comments");
180        }
181        
182        String[] cTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
183        for (String cTypeId : cTypes)
184        {
185            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
186            cType.saxContentTypeAdditionalData(contentHandler, content);
187        }
188        
189        // FIXME CMS-3057
190        Request request = ObjectModelHelper.getRequest(objectModel);
191        boolean displayWorkflow = !"true".equals(request.getParameter("ignore-workflow"));
192        
193        if (displayWorkflow)
194        {
195            _saxWorkflowStep(content);
196        }
197        
198        _saxLanguage (content);
199        
200        _saxDublinCoreMetadata(content);
201
202        _saxContentComments(content);
203
204        _saxOtherData(content);
205        
206        XMLUtils.endElement(contentHandler, "content");
207    }
208    
209    /**
210     * Add attribute if value is not null
211     * @param attrs The attributes
212     * @param name The name of attribute
213     * @param value The value
214     */
215    protected void _addAttributeIfNotNull (AttributesImpl attrs, String name, String value)
216    {
217        if (value != null)
218        {
219            attrs.addCDATAAttribute(name, value);
220        }
221    }
222    
223    /**
224     * SAX the comments of the content
225     * @param content The content to consider. Cannot be null.
226     * @throws SAXException if an error occurs while SAXing.
227     */
228    protected void _saxContentComments(Content content) throws SAXException
229    {
230        if (content instanceof CommentableContent)
231        {
232            CommentableContent cContent = (CommentableContent) content;
233            
234            List<Comment> comments = cContent.getComments(false, true);
235            if (comments.size() > 0)
236            {
237                XMLUtils.startElement(contentHandler, "comments");
238                
239                for (Comment comment : comments)
240                {
241                    AttributesImpl attrs = new AttributesImpl();
242
243                    attrs.addCDATAAttribute("id", comment.getId());
244
245                    attrs.addCDATAAttribute("creation-date", ParameterHelper.valueToString(comment.getCreationDate()));
246
247                    if (!StringUtils.isBlank(comment.getAuthorName()))
248                    {
249                        attrs.addCDATAAttribute("author-name", comment.getAuthorName());
250                    }
251                    
252                    if (!comment.isEmailHidden() && !StringUtils.isBlank(comment.getAuthorEmail()))
253                    {
254                        attrs.addCDATAAttribute("author-email", comment.getAuthorEmail());
255                    }
256
257                    if (!StringUtils.isBlank(comment.getAuthorURL()))
258                    {
259                        attrs.addCDATAAttribute("author-url", comment.getAuthorURL());
260                    }
261
262                    XMLUtils.startElement(contentHandler, "comment", attrs);
263
264                    if (comment.getContent() != null)
265                    {
266                        String[] contents = comment.getContent().split("\r?\n");
267                        for (String c : contents)
268                        {
269                            XMLUtils.createElement(contentHandler, "p", c);
270                        }
271                    }
272
273                    XMLUtils.endElement(contentHandler, "comment");
274                }
275                
276                XMLUtils.endElement(contentHandler, "comments");
277            }
278        }        
279    }
280    
281    /**
282     * SAX the content language
283     * @param content The content
284     * @throws SAXException if an error occurs while SAXing.
285     */
286    protected void _saxLanguage (Content content) throws SAXException
287    {
288        String code = content.getLanguage();
289        Language language = _languageManager.getLanguage(code);
290        
291        AttributesImpl atts = new AttributesImpl();
292        atts.addCDATAAttribute("code", code);
293        
294        if (language != null)
295        {
296            atts.addCDATAAttribute("icon-small", language.getSmallIcon());
297            atts.addCDATAAttribute("icon-medium", language.getMediumIcon());
298            atts.addCDATAAttribute("icon-large", language.getLargeIcon());
299        }
300        
301        XMLUtils.startElement(contentHandler, "content-language", atts);
302        if (language != null)
303        {
304            language.getLabel().toSAX(contentHandler);
305        }
306        XMLUtils.endElement(contentHandler, "content-language");
307        
308    }
309
310    /**
311     * SAX the workflow step if the content is a <code>WorkflowAwareContent</code>
312     * @param content The content
313     * @throws SAXException if an error occurs while SAXing.
314     */
315    protected void _saxWorkflowStep (Content content) throws SAXException
316    {
317        if (content instanceof WorkflowAwareContent)
318        {
319            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
320            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
321            
322            try
323            {
324                long workflowId = waContent.getWorkflowId();
325                String workflowName = workflow.getWorkflowName(workflowId);
326                
327                Step currentStep = _getCurrentStep(waContent, workflow);
328                
329                int currentStepId = currentStep.getStepId();
330                
331                I18nizableText workflowStepName = new I18nizableText("application",  _worklflowHelper.getStepName(workflowName, currentStepId));
332                
333                AttributesImpl atts = new AttributesImpl();
334                atts.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId));
335                if ("application".equals(workflowStepName.getCatalogue()))
336                {
337                    atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png");
338                    atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png");
339                    atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png");
340                }
341                else
342                {
343                    String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
344                    atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png");
345                    atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png");
346                    atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png");
347                }
348                
349                XMLUtils.startElement(contentHandler, "workflow-step", atts);
350                workflowStepName.toSAX(contentHandler);
351                XMLUtils.endElement(contentHandler, "workflow-step");
352            }
353            catch (AmetysRepositoryException e)
354            {
355                // Current step id was not positioned
356            }
357            catch (WorkflowException e)
358            {
359                // Ignore, just don't SAX the workflow step.
360            }
361        }
362    }
363    
364    /**
365     * SAX content metadata.
366     * @param content the content.
367     * @param metadataSet the metadata set.
368     * @throws SAXException if an error occurs while SAXing.
369     * @throws IOException if an error occurs.
370     * @throws ProcessingException if an error occurs.
371     */
372    protected void _saxMetadata(Content content, MetadataSet metadataSet) throws SAXException, ProcessingException, IOException
373    {
374        _metadataManager.saxMetadata(contentHandler, content, metadataSet);
375    }
376    
377    /**
378     * SAX metadata comments.
379     * @param content the content.
380     * @param metadataSet the metadata set.
381     * @throws SAXException if an error occurs while SAXing.
382     * @throws IOException if an error occurs.
383     * @throws ProcessingException if an error occurs.
384     */
385    protected void _saxMetadataComments(Content content, MetadataSet metadataSet) throws SAXException, ProcessingException, IOException
386    {
387        _metadataManager.saxMetadataComments(contentHandler, content, metadataSet);
388    }
389    
390    /**
391     * SAX content Dublin Core metadata.
392     * @param dcObject the Dublin Core object.
393     * @throws SAXException if an error occurs while SAXing.
394     */
395    protected void _saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject) throws SAXException
396    {
397        XMLUtils.startElement(contentHandler, "dublin-core-metadata");
398        _saxIfNotNull("title", dcObject.getDCTitle());
399        _saxIfNotNull("creator", dcObject.getDCCreator());
400        _saxIfNotNull("subject", dcObject.getDCSubject());
401        _saxIfNotNull("description", dcObject.getDCDescription());
402        _saxIfNotNull("publisher", dcObject.getDCPublisher());
403        _saxIfNotNull("contributor", dcObject.getDCContributor());
404        _saxIfNotNull("date", dcObject.getDCDate());
405        _saxIfNotNull("type", dcObject.getDCType());
406        _saxIfNotNull("format", dcObject.getDCFormat());
407        _saxIfNotNull("identifier", dcObject.getDCIdentifier());
408        _saxIfNotNull("source", dcObject.getDCSource());
409        _saxIfNotNull("language", dcObject.getDCLanguage());
410        _saxIfNotNull("relation", dcObject.getDCRelation());
411        _saxIfNotNull("coverage", dcObject.getDCCoverage());
412        _saxIfNotNull("rights", dcObject.getDCRights());
413        XMLUtils.endElement(contentHandler, "dublin-core-metadata");
414    }
415    
416    /**
417     * SAX string Dublin Core metadata.
418     * @param name the metadata name.
419     * @param value the metadata value.
420     * @throws SAXException if an error occurs while SAXing.
421     */
422    protected void _saxIfNotNull(String name, String value) throws SAXException
423    {
424        if (value != null)
425        {
426            XMLUtils.createElement(contentHandler, name, value);
427        }
428    }
429    
430    /**
431     * SAX string Dublin Core metadata.
432     * @param name the metadata name.
433     * @param values the metadata values.
434     * @throws SAXException if an error occurs while SAXing.
435     */
436    protected void _saxIfNotNull(String name, String[] values) throws SAXException
437    {
438        if (values != null)
439        {
440            for (String value : values)
441            {
442                XMLUtils.createElement(contentHandler, name, value);
443            }
444        }
445    }
446    
447    /**
448     * SAX date Dublin Core metadata.
449     * @param name the metadata name.
450     * @param value the metadata value.
451     * @throws SAXException if an error occurs while SAXing.
452     */
453    protected void _saxIfNotNull(String name, Date value) throws SAXException
454    {
455        if (value != null)
456        {
457            LocalDate ld = DateUtils.asLocalDate(value);
458            XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE));
459        }
460    }
461    
462    /**
463     * SAX any other data needed by the view.<p>
464     * Default implementation does nothing.
465     * @param content the content.
466     * @throws SAXException if an error occurs while SAXing.
467     * @throws ProcessingException if an error occurs.
468     */
469    protected void _saxOtherData(Content content) throws SAXException, ProcessingException
470    {
471        // No other data to SAX
472    }
473    
474    /**
475     * Retrieves the metadata set to be used when SAX'ing metadata and metadata comments.
476     * @param content The content to consider. Cannot be null.
477     * @return The retrieved metadata set
478     * @throws ProcessingException If the metadata set could not be retrieved
479     */
480    protected MetadataSet _getMetadataSet(Content content) throws ProcessingException
481    {
482        boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false);
483        String metadataSetName = parameters.getParameter("metadataSetName", "");
484        if (StringUtils.isBlank(metadataSetName))
485        {
486            metadataSetName = "main";
487        }
488        
489        MetadataSet metadataSet = null;
490        
491        if (isEditionMetadataSet)
492        {
493            metadataSet = _cTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
494        }
495        else
496        {
497            metadataSet = _cTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
498        }
499        
500        if (metadataSet == null)
501        {
502            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type(s) '%s'",
503                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ','))); 
504        }
505        
506        return metadataSet;
507    }
508    
509    /**
510     * Get a content's step, wherever it works on the base version or not.
511     * @param content the content.
512     * @param workflow The workflow impl to use 
513     * @return the content's workflow step.
514     * @throws WorkflowException if an error occurs.
515     */
516    protected Step _getCurrentStep(WorkflowAwareContent content, AmetysObjectWorkflow workflow) throws WorkflowException
517    {
518        long workflowId = content.getWorkflowId();
519        
520        Step currentStep = (Step) workflow.getCurrentSteps(workflowId).get(0);
521        
522        if (content instanceof VersionAwareAmetysObject)
523        {
524            VersionAwareAmetysObject vaContent = (VersionAwareAmetysObject) content;
525            String currentRevision = vaContent.getRevision();
526            
527            if (currentRevision != null)
528            {
529                
530                String[] allRevisions = vaContent.getAllRevisions();
531                int currentRevIndex = ArrayUtils.indexOf(allRevisions, currentRevision);
532                
533                if (currentRevIndex > -1 && currentRevIndex < (allRevisions.length - 1))
534                {
535                    String nextRevision = allRevisions[currentRevIndex + 1];
536                    
537                    Date currentRevTimestamp = vaContent.getRevisionTimestamp();
538                    Date nextRevTimestamp = vaContent.getRevisionTimestamp(nextRevision);
539                    
540                    // Get all steps between the two revisions. 
541                    List<Step> steps = _worklflowHelper.getStepsBetween(workflow, workflowId, currentRevTimestamp, nextRevTimestamp);
542                    
543                    // In the old workflow structure
544                    // We take the second, which is current revision's last step.
545                    if (steps.size() > 0 && steps.get(0) instanceof AmetysStep)
546                    {
547                        AmetysStep amStep = (AmetysStep) steps.get(0);
548                        if (amStep.getProperty("actionFinishDate") != null)
549                        {
550                            // New workflow structure detected: cut the first workflow step
551                            // in the list, as it belongs to the next version.
552                            steps = steps.subList(1, steps.size());
553                        }
554                    }
555                    
556                    // Order by step descendant.
557                    Collections.sort(steps, new Comparator<Step>()
558                    {
559                        public int compare(Step step1, Step step2)
560                        {
561                            return -new Long(step1.getId()).compareTo(step2.getId());
562                        }
563                    });
564                    
565                    // The first step in the list is the current version's last workflow step.
566                    if (steps.size() > 0)
567                    {
568                        currentStep = steps.get(0);
569                    }
570                }
571            }
572        }
573        return currentStep;
574    }
575    
576}