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