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