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