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