001/*
002 *  Copyright 2019 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.time.LocalDate;
019import java.time.format.DateTimeFormatter;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.Date;
023import java.util.List;
024import java.util.Locale;
025import java.util.Set;
026
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang.ArrayUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.xml.sax.ContentHandler;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.contenttype.ContentTypesHelper;
042import org.ametys.cms.languages.Language;
043import org.ametys.cms.languages.LanguagesManager;
044import org.ametys.cms.repository.Content;
045import org.ametys.cms.repository.ReactionableObject;
046import org.ametys.cms.repository.ReactionableObjectHelper;
047import org.ametys.cms.repository.ReportableObject;
048import org.ametys.cms.repository.ReportableObjectHelper;
049import org.ametys.cms.repository.WorkflowAwareContent;
050import org.ametys.cms.repository.comment.Comment;
051import org.ametys.cms.repository.comment.CommentableContent;
052import org.ametys.cms.repository.comment.CommentsDAO;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.core.util.DateUtils;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
057import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject;
058import org.ametys.plugins.repository.jcr.JCRAmetysObject;
059import org.ametys.plugins.repository.model.RepositoryDataContext;
060import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
061import org.ametys.plugins.workflow.store.AmetysStep;
062import org.ametys.plugins.workflow.support.WorkflowHelper;
063import org.ametys.plugins.workflow.support.WorkflowProvider;
064import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.runtime.model.View;
067
068import com.opensymphony.workflow.WorkflowException;
069import com.opensymphony.workflow.spi.Step;
070
071/**
072 * Component responsible for generating SAX events representing a {@link Content}.
073 */
074public class ContentSaxer implements Serviceable, Component 
075{
076    /** Avalon role. */
077    public static final String CMS_CONTENT_SAXER_ROLE = ContentSaxer.class.getName();
078    
079    private WorkflowProvider _workflowProvider;
080    private WorkflowHelper _worklflowHelper;
081    private ContentTypesHelper _contentTypesHelper; 
082    private LanguagesManager _languageManager;
083    private ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
084    private CommentsDAO _commentsDAO;
085    private ReactionableObjectHelper _reactionableHelper;
086
087    @Override
088    public void service(ServiceManager manager) throws ServiceException
089    {
090        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
091        _worklflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
092        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
093        _languageManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
094        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
095        _commentsDAO = (CommentsDAO) manager.lookup(CommentsDAO.ROLE);
096        _reactionableHelper = (ReactionableObjectHelper) manager.lookup(ReactionableObjectHelper.ROLE);
097    }
098    
099    /**
100     * Generates SAX events representing a {@link Content}. 
101     * <br>When called with a non null tag name, a surrounding element will be generated, 
102     * along with XML attributes representing the content's metadata (creation/modification/validation dates and authors, ...).
103     * @param content the {@link Content}.
104     * @param contentHandler the ContentHandler receving SAX events.
105     * @param locale the {@link Locale} to use for eg. multilingual attributes.
106     * @param view the View or null to select all attributes.
107     * @param tagName the surrounding tag name or null to SAX events without root tag.
108     * @param saxWorkflowStep if true, also produces SAX events for the current workflow step.
109     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
110     * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language.
111     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
112     * @throws SAXException if an error occurs during the SAX events generation.
113     */
114    public void saxContent(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName) throws SAXException
115    {
116        saxContent(content, contentHandler, locale, view, tagName, saxWorkflowStep, saxWorkflowInfo, saxLanguageInfo, attributesTagName, false);
117    }
118    
119    /**
120     * Generates SAX events representing a {@link Content}. 
121     * <br>When called with a non null tag name, a surrounding element will be generated, 
122     * along with XML attributes representing the content's metadata (creation/modification/validation dates and authors, ...).
123     * @param content the {@link Content}.
124     * @param contentHandler the ContentHandler receving SAX events.
125     * @param locale the {@link Locale} to use for eg. multilingual attributes.
126     * @param view the View or null to select all attributes.
127     * @param tagName the surrounding tag name or null to SAX events without root tag.
128     * @param saxWorkflowStep if true, also produces SAX events for the current workflow step.
129     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
130     * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language.
131     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
132     * @param isEdition <code>true</code> if SAX events are generated in edition mode, <code>false</code> otherwise
133     * @throws SAXException if an error occurs during the SAX events generation.
134     */
135    public void saxContent(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName, boolean isEdition) throws SAXException
136    {
137        if (StringUtils.isNotEmpty(tagName))
138        {
139            saxRootTag(content, contentHandler, locale, tagName);
140        }
141        
142        saxBody(content, contentHandler, locale, view, tagName, saxWorkflowStep, saxWorkflowInfo, saxLanguageInfo, attributesTagName, isEdition);
143        
144        if (StringUtils.isNotEmpty(tagName))
145        {
146            XMLUtils.endElement(contentHandler, tagName);
147        }
148    }
149    
150    /**
151     * Generates SAX events for the content data.
152     * @param content the {@link Content}.
153     * @param contentHandler the ContentHandler receving SAX events.
154     * @param locale the {@link Locale} to use for eg. multilingual attributes.
155     * @param view the View or null to select all attributes.
156     * @param tagName the surrounding tag name or null to SAX events without root tag.
157     * @param saxWorkflowStep if true, also produces SAX events for the current workflow step.
158     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
159     * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language.
160     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
161     * @param isEdition <code>true</code> if SAX events are generated in edition mode, <code>false</code> otherwise
162     * @throws SAXException if an error occurs during the SAX events generation.
163     */
164    protected void saxBody(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName, boolean isEdition) throws SAXException
165    {
166        saxContentTypes(content, contentHandler, true);
167        saxAttributes(content, contentHandler, locale, view, tagName, attributesTagName, isEdition);
168        
169        if (saxWorkflowStep || saxWorkflowInfo)
170        {
171            saxWorkflowStep(content, contentHandler, saxWorkflowInfo);
172        }
173        
174        if (saxLanguageInfo)
175        {
176            saxLanguage(content, contentHandler);
177        }
178        
179        saxDublinCoreMetadata(content, contentHandler);
180        
181        if (content instanceof CommentableContent)
182        {
183            saxContentComments((CommentableContent) content, contentHandler);
184        }
185
186        if (content instanceof ReactionableObject)
187        {
188            _reactionableHelper.saxReactions((ReactionableObject) content, contentHandler);
189        }
190        
191        if (content instanceof ReportableObject)
192        {
193            ReportableObjectHelper.saxReports((ReportableObject) content, contentHandler);
194        }
195    }
196    
197    /**
198     * Generates a surrounding tag, with content metadata.
199     * @param content the {@link Content}.
200     * @param contentHandler the ContentHandler receving SAX events.
201     * @param locale the {@link Locale} to use for eg. multilingual attributes.
202     * @param tagName the surrounding tag name or null to SAX events without root tag.
203     * @throws SAXException if an error occurs during the SAX events generation.
204     */
205    protected void saxRootTag(Content content, ContentHandler contentHandler, Locale locale, String tagName) throws SAXException
206    {
207        AttributesImpl attrs = new AttributesImpl();
208        
209        if (content instanceof JCRAmetysObject)
210        {
211            _addJcrAttributes((JCRAmetysObject) content, attrs);
212        }
213        
214        attrs.addCDATAAttribute("id", content.getId());
215        attrs.addCDATAAttribute("name", content.getName());
216        attrs.addCDATAAttribute("title", content.getTitle(locale));
217        if (content.getLanguage() != null)
218        {
219            attrs.addCDATAAttribute("language", content.getLanguage());
220        }
221        attrs.addCDATAAttribute("createdAt", DateUtils.dateToString(content.getCreationDate()));
222        attrs.addCDATAAttribute("creator", UserIdentity.userIdentityToString(content.getCreator()));
223        attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
224        
225        Date lastValidatedAt = content.getLastValidationDate();
226        if (lastValidatedAt != null)
227        {
228            attrs.addCDATAAttribute("lastValidatedAt", DateUtils.dateToString(lastValidatedAt));
229        }
230        
231        attrs.addCDATAAttribute("lastContributor", UserIdentity.userIdentityToString(content.getLastContributor()));
232        attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent));
233        
234        addAttributeIfNotNull (attrs, "iconGlyph", _contentTypesHelper.getIconGlyph(content));
235        addAttributeIfNotNull (attrs, "iconDecorator", _contentTypesHelper.getIconDecorator(content));
236        
237        addAttributeIfNotNull (attrs, "smallIcon", _contentTypesHelper.getSmallIcon(content));
238        addAttributeIfNotNull (attrs, "mediumIcon", _contentTypesHelper.getMediumIcon(content));
239        addAttributeIfNotNull (attrs, "largeIcon", _contentTypesHelper.getLargeIcon(content));
240        
241        XMLUtils.startElement(contentHandler, tagName, attrs);
242    }
243    
244    private void _addJcrAttributes(JCRAmetysObject content, AttributesImpl attrs)
245    {
246        Node node = content.getNode();
247        try
248        {
249            attrs.addCDATAAttribute("uuid", node.getIdentifier());
250        }
251        catch (RepositoryException e)
252        {
253            throw new IllegalArgumentException("Unable to get jcr UUID for content '" + content.getId() + "'", e);
254        }
255        
256        try
257        {
258            attrs.addCDATAAttribute("primaryType", node.getPrimaryNodeType().getName());
259        }
260        catch (RepositoryException e)
261        {
262            throw new IllegalArgumentException("Unable to get jcr Primary Type for content '" + content.getId() + "'", e);
263        }
264    }
265    
266    /**
267     * Generates SAX events for {@link Content#getTypes content types}, and possibly {@link Content#getMixinTypes mixin types}
268     * @param content the {@link Content}.
269     * @param contentHandler the ContentHandler receving SAX events.
270     * @param saxMixins if true, also produces SAX events for {@link Content#getMixinTypes mixin types}.
271     * @throws SAXException if an error occurs during the SAX events generation.
272     */
273    protected void saxContentTypes(Content content, ContentHandler contentHandler, boolean saxMixins) throws SAXException
274    {
275        _saxContentTypes(content, contentHandler);
276        if (saxMixins)
277        {
278            _saxMixins(content, contentHandler);
279        }
280    }
281    
282    private void _saxContentTypes(Content content, ContentHandler contentHandler) throws SAXException
283    {
284        String contentTypesTagName = "contentTypes";
285        String singleContentTypeTagName = "contentType";
286        XMLUtils.startElement(contentHandler, contentTypesTagName);
287        for (String contentType : content.getTypes())
288        {
289            XMLUtils.createElement(contentHandler, singleContentTypeTagName, contentType);
290        }
291        XMLUtils.endElement(contentHandler, contentTypesTagName);
292    }
293    
294    private void _saxMixins(Content content, ContentHandler contentHandler) throws SAXException
295    {
296        String mixinsTagName = "mixins";
297        String singleMixinTagName = "mixin";
298        XMLUtils.startElement(contentHandler, mixinsTagName);
299        for (String mixinType : content.getMixinTypes())
300        {
301            XMLUtils.createElement(contentHandler, singleMixinTagName, mixinType);
302        }
303        XMLUtils.endElement(contentHandler, mixinsTagName);
304    }
305    
306    /**
307     * Generates SAX events for actual content's data.
308     * @param content the {@link Content}.
309     * @param contentHandler the ContentHandler receving SAX events.
310     * @param locale the {@link Locale} to use for eg. multilingual attributes.
311     * @param view the View or null to select all attributes.
312     * @param tagName the surrounding tag name or null to SAX events without root tag.
313     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
314     * @param isEdition <code>true</code> if SAX events are generated in edition mode, <code>false</code> otherwise
315     * @throws SAXException if an error occurs during the SAX events generation.
316     */
317    protected void saxAttributes(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, String attributesTagName, boolean isEdition) throws SAXException
318    {
319        XMLUtils.startElement(contentHandler, attributesTagName);
320
321        RepositoryDataContext context = RepositoryDataContext.newInstance();
322        context.withLocale(locale);
323
324        if (view == null)
325        {
326            content.dataToSAX(contentHandler, context.withEmptyValues(false));
327        }
328        else
329        {
330            if (isEdition)
331            {
332                Set<String> externalizableData = _externalizableDataProviderEP.getExternalizableDataPaths(content);
333                content.dataToSAXForEdition(contentHandler, view, context.withExternalizableData(externalizableData));
334            }
335            else
336            {
337                content.dataToSAX(contentHandler, view, context.withEmptyValues(false));
338            }
339        }
340
341        XMLUtils.endElement(contentHandler, attributesTagName);
342    }
343    
344    /**
345     * Generates SAX events representing the current workflow step.
346     * @param content the {@link Content}.
347     * @param contentHandler the ContentHandler receiving SAX events.
348     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
349     * @throws SAXException if an error occurs during the SAX events generation.
350     */
351    protected void saxWorkflowStep(Content content, ContentHandler contentHandler, boolean saxWorkflowInfo) throws SAXException
352    {
353        if (content instanceof WorkflowAwareContent)
354        {
355            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
356            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
357            
358            try
359            {
360                long workflowId = waContent.getWorkflowId();
361                String workflowName = workflow.getWorkflowName(workflowId);
362                
363                Step currentStep = getCurrentStep(waContent, workflow);
364                
365                int currentStepId = currentStep.getStepId();
366                
367                I18nizableText workflowStepName = new I18nizableText("application",  _worklflowHelper.getStepName(workflowName, currentStepId));
368                
369                AttributesImpl atts = new AttributesImpl();
370                atts.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId));
371                atts.addAttribute("", "workflowName", "workflowName", "CDATA", String.valueOf(workflowName));
372                
373                if (saxWorkflowInfo)
374                {
375                    if ("application".equals(workflowStepName.getCatalogue()))
376                    {
377                        atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png");
378                        atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png");
379                        atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png");
380                    }
381                    else
382                    {
383                        String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
384                        atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png");
385                        atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png");
386                        atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png");
387                    }
388                }
389                
390                XMLUtils.startElement(contentHandler, "workflow-step", atts);
391                
392                if (saxWorkflowInfo)
393                {
394                    workflowStepName.toSAX(contentHandler);
395                }
396                
397                XMLUtils.endElement(contentHandler, "workflow-step");
398            }
399            catch (AmetysRepositoryException e)
400            {
401                // Current step id was not positioned
402            }
403            catch (WorkflowException e)
404            {
405                // Ignore, just don't SAX the workflow step.
406            }
407        }
408    }
409
410    /**
411     * Get the current workflow step of the content.
412     * @param content the {@link Content}.
413     * @param workflow the associated workflow.
414     * @return the current step
415     * @throws WorkflowException if somethng got wrong processing workflow data.
416     */
417    protected Step getCurrentStep(WorkflowAwareContent content, AmetysObjectWorkflow workflow) throws WorkflowException
418    {
419        long workflowId = content.getWorkflowId();
420        
421        Step currentStep = (Step) workflow.getCurrentSteps(workflowId).get(0);
422        
423        if (content instanceof VersionAwareAmetysObject)
424        {
425            VersionAwareAmetysObject vaContent = (VersionAwareAmetysObject) content;
426            String currentRevision = vaContent.getRevision();
427            
428            if (currentRevision != null)
429            {
430                
431                String[] allRevisions = vaContent.getAllRevisions();
432                int currentRevIndex = ArrayUtils.indexOf(allRevisions, currentRevision);
433                
434                if (currentRevIndex > -1 && currentRevIndex < (allRevisions.length - 1))
435                {
436                    String nextRevision = allRevisions[currentRevIndex + 1];
437                    
438                    Date currentRevTimestamp = vaContent.getRevisionTimestamp();
439                    Date nextRevTimestamp = vaContent.getRevisionTimestamp(nextRevision);
440                    
441                    // Get all steps between the two revisions. 
442                    List<Step> steps = _worklflowHelper.getStepsBetween(workflow, workflowId, currentRevTimestamp, nextRevTimestamp);
443                    
444                    // In the old workflow structure
445                    // We take the second, which is current revision's last step.
446                    if (steps.size() > 0 && steps.get(0) instanceof AmetysStep)
447                    {
448                        AmetysStep amStep = (AmetysStep) steps.get(0);
449                        if (amStep.getProperty("actionFinishDate") != null)
450                        {
451                            // New workflow structure detected: cut the first workflow step
452                            // in the list, as it belongs to the next version.
453                            steps = steps.subList(1, steps.size());
454                        }
455                    }
456                    
457                    // Order by step descendant.
458                    Collections.sort(steps, new Comparator<Step>()
459                    {
460                        public int compare(Step step1, Step step2)
461                        {
462                            return -Long.valueOf(step1.getId()).compareTo(step2.getId());
463                        }
464                    });
465                    
466                    // The first step in the list is the current version's last workflow step.
467                    if (steps.size() > 0)
468                    {
469                        currentStep = steps.get(0);
470                    }
471                }
472            }
473        }
474        
475        return currentStep;
476    }
477    
478    /**
479     * Generates SAX events for the content's language.
480     * @param content the {@link Content}.
481     * @param contentHandler the ContentHandler receving SAX events.
482     * @throws SAXException if an error occurs during the SAX events generation.
483     */
484    protected void saxLanguage(Content content, ContentHandler contentHandler) throws SAXException
485    {
486        String code = content.getLanguage();
487        if (code != null)
488        {
489            Language language = _languageManager.getLanguage(code);
490            
491            AttributesImpl atts = new AttributesImpl();
492            atts.addCDATAAttribute("code", code);
493            
494            if (language != null)
495            {
496                atts.addCDATAAttribute("icon-small", language.getSmallIcon());
497                atts.addCDATAAttribute("icon-medium", language.getMediumIcon());
498                atts.addCDATAAttribute("icon-large", language.getLargeIcon());
499            }
500            
501            XMLUtils.startElement(contentHandler, "content-language", atts);
502            if (language != null)
503            {
504                language.getLabel().toSAX(contentHandler);
505            }
506            XMLUtils.endElement(contentHandler, "content-language");
507        }
508    }
509    
510    /**
511     * Generates SAX events for the DC metadata.
512     * @param dcObject the {@link Content}.
513     * @param contentHandler the ContentHandler receving SAX events.
514     * @throws SAXException if an error occurs during the SAX events generation.
515     */
516    protected void saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject, ContentHandler contentHandler) throws SAXException
517    {
518        XMLUtils.startElement(contentHandler, "dublin-core-metadata");
519        saxIfNotNull("title", dcObject.getDCTitle(), contentHandler);
520        saxIfNotNull("creator", dcObject.getDCCreator(), contentHandler);
521        saxIfNotNull("subject", dcObject.getDCSubject(), contentHandler);
522        saxIfNotNull("description", dcObject.getDCDescription(), contentHandler);
523        saxIfNotNull("publisher", dcObject.getDCPublisher(), contentHandler);
524        saxIfNotNull("contributor", dcObject.getDCContributor(), contentHandler);
525        saxIfNotNull("date", dcObject.getDCDate(), contentHandler);
526        saxIfNotNull("type", dcObject.getDCType(), contentHandler);
527        saxIfNotNull("format", dcObject.getDCFormat(), contentHandler);
528        saxIfNotNull("identifier", dcObject.getDCIdentifier(), contentHandler);
529        saxIfNotNull("source", dcObject.getDCSource(), contentHandler);
530        saxIfNotNull("language", dcObject.getDCLanguage(), contentHandler);
531        saxIfNotNull("relation", dcObject.getDCRelation(), contentHandler);
532        saxIfNotNull("coverage", dcObject.getDCCoverage(), contentHandler);
533        saxIfNotNull("rights", dcObject.getDCRights(), contentHandler);
534        XMLUtils.endElement(contentHandler, "dublin-core-metadata");
535    }
536
537    /**
538     * Send a value if not null.
539     * @param name the tag name.
540     * @param value the value.
541     * @param contentHandler the ContentHandler receving SAX events.
542     * @throws SAXException if an error occurs during the SAX events generation.
543     */
544    protected void saxIfNotNull(String name, String value, ContentHandler contentHandler) throws SAXException
545    {
546        if (value != null)
547        {
548            XMLUtils.createElement(contentHandler, name, value);
549        }
550    }
551    
552    /**
553     * Send values if not null.
554     * @param name the tag name.
555     * @param values the values.
556     * @param contentHandler the ContentHandler receving SAX events.
557     * @throws SAXException if an error occurs during the SAX events generation.
558     */
559    protected void saxIfNotNull(String name, String[] values, ContentHandler contentHandler) throws SAXException
560    {
561        if (values != null)
562        {
563            for (String value : values)
564            {
565                XMLUtils.createElement(contentHandler, name, value);
566            }
567        }
568    }
569    
570    /**
571     * Send a value if not null.
572     * @param name the tag name.
573     * @param value the value.
574     * @param contentHandler the ContentHandler receving SAX events.
575     * @throws SAXException if an error occurs during the SAX events generation.
576     */
577    protected void saxIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException
578    {
579        if (value != null)
580        {
581            LocalDate ld = DateUtils.asLocalDate(value);
582            XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE));
583        }
584    }
585    
586    /**
587     * Generates SAX events for content's comments.
588     * @param content the {@link Content}.
589     * @param contentHandler the ContentHandler receving SAX events.
590     * @throws SAXException if an error occurs during the SAX events generation.
591     */
592    protected void saxContentComments(CommentableContent content, ContentHandler contentHandler) throws SAXException
593    {
594        List<Comment> comments = content.getComments(false, true);
595        
596        if (comments.size() > 0)
597        {
598            XMLUtils.startElement(contentHandler, "comments");
599            for (Comment comment : comments)
600            {
601                _commentsDAO.saxComment(contentHandler, comment, 0);
602            }
603            XMLUtils.endElement(contentHandler, "comments");
604        }
605    }
606    
607    /**
608     * Add attribute if value is not null
609     * @param attrs The attributes
610     * @param name The name of attribute
611     * @param value The value
612     */
613    protected void addAttributeIfNotNull (AttributesImpl attrs, String name, String value)
614    {
615        if (value != null)
616        {
617            attrs.addCDATAAttribute(name, value);
618        }
619    }
620}