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