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.ZonedDateTime;
021import java.time.format.DateTimeFormatter;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.Date;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.function.Consumer;
030import java.util.function.Function;
031import java.util.stream.IntStream;
032
033import javax.jcr.Node;
034import javax.jcr.RepositoryException;
035import javax.xml.transform.TransformerException;
036
037import org.apache.avalon.framework.component.Component;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.xml.AttributesImpl;
042import org.apache.cocoon.xml.XMLUtils;
043import org.apache.commons.lang.ArrayUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.xpath.XPathAPI;
046import org.apache.xpath.objects.XObject;
047import org.w3c.dom.Element;
048import org.w3c.dom.NodeList;
049import org.xml.sax.ContentHandler;
050import org.xml.sax.SAXException;
051
052import org.ametys.cms.languages.Language;
053import org.ametys.cms.languages.LanguagesManager;
054import org.ametys.cms.repository.Content;
055import org.ametys.cms.repository.ModifiableContent;
056import org.ametys.cms.repository.ReactionableObject;
057import org.ametys.cms.repository.ReactionableObject.ReactionType;
058import org.ametys.cms.repository.ReportableObject;
059import org.ametys.cms.repository.WorkflowAwareContent;
060import org.ametys.cms.repository.comment.Comment;
061import org.ametys.cms.repository.comment.CommentableContent;
062import org.ametys.core.user.UserIdentity;
063import org.ametys.core.util.DateUtils;
064import org.ametys.core.util.LambdaUtils.ThrowingFunction;
065import org.ametys.plugins.core.user.UserHelper;
066import org.ametys.plugins.repository.AmetysRepositoryException;
067import org.ametys.plugins.repository.data.extractor.xml.ModelAwareXMLValuesExtractor;
068import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter;
069import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject;
070import org.ametys.plugins.repository.dublincore.ModifiableDublinCoreAwareAmetysObject;
071import org.ametys.plugins.repository.jcr.JCRAmetysObject;
072import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
073import org.ametys.plugins.workflow.store.AmetysStep;
074import org.ametys.plugins.workflow.support.WorkflowHelper;
075import org.ametys.plugins.workflow.support.WorkflowProvider;
076import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
077import org.ametys.runtime.i18n.I18nizableText;
078import org.ametys.runtime.model.Model;
079import org.ametys.runtime.model.View;
080
081import com.opensymphony.workflow.WorkflowException;
082import com.opensymphony.workflow.spi.Step;
083
084/**
085 * Component responsible for generating SAX events representing a {@link Content}.
086 */
087public class ContentSaxer implements Serviceable, Component 
088{
089    /** Avalon role. */
090    public static final String ROLE = ContentSaxer.class.getName();
091    
092    private WorkflowProvider _workflowProvider;
093    private WorkflowHelper _worklflowHelper;
094    private LanguagesManager _languageManager;
095    private UserHelper _userHelper;
096
097    @Override
098    public void service(ServiceManager manager) throws ServiceException
099    {
100        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
101        _worklflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
102        _languageManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
103        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
104    }
105    
106    /**
107     * Generates SAX events representing a {@link Content}. 
108     * <br>When called with a non null tag name, a surrounding element will be generated, 
109     * along with XML attributes representing the content's metadata (creation/modification/validation dates and authors, ...).
110     * @param content the {@link Content}.
111     * @param contentHandler the ContentHandler receving SAX events.
112     * @param locale the {@link Locale} to use for eg. multilingual attributes.
113     * @param view the View or null to select all attributes.
114     * @param tagName the surrounding tag name or null to SAX events without root tag.
115     * @param saxWorkflowStep if true, also produces SAX events for the current workflow step.
116     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
117     * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language.
118     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
119     * @throws SAXException if an error occurs during the SAX events generation.
120     */
121    public void saxContent(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName) throws SAXException
122    {
123        if (StringUtils.isNotEmpty(tagName))
124        {
125            saxRootTag(content, contentHandler, locale, tagName);
126        }
127        
128        saxBody(content, contentHandler, locale, view, tagName, saxWorkflowStep, saxWorkflowInfo, saxLanguageInfo, attributesTagName);
129        
130        if (StringUtils.isNotEmpty(tagName))
131        {
132            XMLUtils.endElement(contentHandler, tagName);
133        }
134    }
135    
136    /**
137     * Generates SAX events for the content data.
138     * @param content the {@link Content}.
139     * @param contentHandler the ContentHandler receving SAX events.
140     * @param locale the {@link Locale} to use for eg. multilingual attributes.
141     * @param view the View or null to select all attributes.
142     * @param tagName the surrounding tag name or null to SAX events without root tag.
143     * @param saxWorkflowStep if true, also produces SAX events for the current workflow step.
144     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
145     * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language.
146     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
147     * @throws SAXException if an error occurs during the SAX events generation.
148     */
149    protected void saxBody(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName) throws SAXException
150    {
151        saxContentTypes(content, contentHandler, true);
152        saxAttributes(content, contentHandler, locale, view, tagName, attributesTagName);
153        
154        if (saxWorkflowStep || saxWorkflowInfo)
155        {
156            saxWorkflowStep(content, contentHandler, saxWorkflowInfo);
157        }
158        
159        if (saxLanguageInfo)
160        {
161            saxLanguage(content, contentHandler);
162        }
163        
164        saxDublinCoreMetadata(content, contentHandler);
165        
166        if (content instanceof CommentableContent)
167        {
168            saxContentComments((CommentableContent) content, contentHandler);
169        }
170
171        if (content instanceof ReactionableObject)
172        {
173            saxReactions((ReactionableObject) content, contentHandler);
174        }
175        
176        if (content instanceof ReportableObject)
177        {
178            saxReports((ReportableObject) content, contentHandler);
179        }
180    }
181    
182    /**
183     * Generates a surrounding tag, with content metadata.
184     * @param content the {@link Content}.
185     * @param contentHandler the ContentHandler receving SAX events.
186     * @param locale the {@link Locale} to use for eg. multilingual attributes.
187     * @param tagName the surrounding tag name or null to SAX events without root tag.
188     * @throws SAXException if an error occurs during the SAX events generation.
189     */
190    protected void saxRootTag(Content content, ContentHandler contentHandler, Locale locale, String tagName) throws SAXException
191    {
192        AttributesImpl attrs = new AttributesImpl();
193        attrs.addCDATAAttribute("id", content.getId());
194        
195        if (content instanceof JCRAmetysObject)
196        {
197            _addJcrAttributes((JCRAmetysObject) content, attrs);
198        }
199        
200        attrs.addCDATAAttribute("id", content.getId());
201        attrs.addCDATAAttribute("name", content.getName());
202        attrs.addCDATAAttribute("title", content.getTitle(locale));
203        if (content.getLanguage() != null)
204        {
205            attrs.addCDATAAttribute("language", content.getLanguage());
206        }
207        attrs.addCDATAAttribute("createdAt", DateUtils.dateToString(content.getCreationDate()));
208        attrs.addCDATAAttribute("creator", UserIdentity.userIdentityToString(content.getCreator()));
209        attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified()));
210        
211        Date lastValidatedAt = content.getLastValidationDate();
212        if (lastValidatedAt != null)
213        {
214            attrs.addCDATAAttribute("lastValidatedAt", DateUtils.dateToString(lastValidatedAt));
215        }
216        
217        attrs.addCDATAAttribute("lastContributor", UserIdentity.userIdentityToString(content.getLastContributor()));
218        attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent));
219        
220        XMLUtils.startElement(contentHandler, tagName, attrs);
221    }
222    
223    private void _addJcrAttributes(JCRAmetysObject content, AttributesImpl attrs)
224    {
225        Node node = content.getNode();
226        try
227        {
228            attrs.addCDATAAttribute("uuid", node.getIdentifier());
229        }
230        catch (RepositoryException e)
231        {
232            throw new IllegalArgumentException("Unable to get jcr UUID for content '" + content.getId() + "'", e);
233        }
234        
235        try
236        {
237            attrs.addCDATAAttribute("primaryType", node.getPrimaryNodeType().getName());
238        }
239        catch (RepositoryException e)
240        {
241            throw new IllegalArgumentException("Unable to get jcr Primary Type for content '" + content.getId() + "'", e);
242        }
243    }
244    
245    /**
246     * Generates SAX events for {@link Content#getTypes content types}, and possibly {@link Content#getMixinTypes mixin types}
247     * @param content the {@link Content}.
248     * @param contentHandler the ContentHandler receving SAX events.
249     * @param saxMixins if true, also produces SAX events for {@link Content#getMixinTypes mixin types}.
250     * @throws SAXException if an error occurs during the SAX events generation.
251     */
252    protected void saxContentTypes(Content content, ContentHandler contentHandler, boolean saxMixins) throws SAXException
253    {
254        _saxContentTypes(content, contentHandler);
255        if (saxMixins)
256        {
257            _saxMixins(content, contentHandler);
258        }
259    }
260    
261    private void _saxContentTypes(Content content, ContentHandler contentHandler) throws SAXException
262    {
263        String contentTypesTagName = "contentTypes";
264        String singleContentTypeTagName = "contentType";
265        XMLUtils.startElement(contentHandler, contentTypesTagName);
266        for (String contentType : content.getTypes())
267        {
268            XMLUtils.createElement(contentHandler, singleContentTypeTagName, contentType);
269        }
270        XMLUtils.endElement(contentHandler, contentTypesTagName);
271    }
272    
273    private void _saxMixins(Content content, ContentHandler contentHandler) throws SAXException
274    {
275        String mixinsTagName = "mixins";
276        String singleMixinTagName = "mixin";
277        XMLUtils.startElement(contentHandler, mixinsTagName);
278        for (String mixinType : content.getMixinTypes())
279        {
280            XMLUtils.createElement(contentHandler, singleMixinTagName, mixinType);
281        }
282        XMLUtils.endElement(contentHandler, mixinsTagName);
283    }
284    
285    /**
286     * Generates SAX events for actual content's data.
287     * @param content the {@link Content}.
288     * @param contentHandler the ContentHandler receving SAX events.
289     * @param locale the {@link Locale} to use for eg. multilingual attributes.
290     * @param view the View or null to select all attributes.
291     * @param tagName the surrounding tag name or null to SAX events without root tag.
292     * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes.
293     * @throws SAXException if an error occurs during the SAX events generation.
294     */
295    protected void saxAttributes(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, String attributesTagName) throws SAXException
296    {
297        try
298        {
299            XMLUtils.startElement(contentHandler, attributesTagName);
300            
301            if (view == null)
302            {
303                content.dataToSAX(contentHandler, locale);
304            }
305            else
306            {
307                content.dataToSAX(contentHandler, view, locale);
308            }
309            
310            XMLUtils.endElement(contentHandler, attributesTagName);
311        }
312        catch (IOException ex)
313        {
314            throw new RuntimeException(ex);
315        }
316    }
317    
318    /**
319     * Generates SAX events representing the current workflow step.
320     * @param content the {@link Content}.
321     * @param contentHandler the ContentHandler receving SAX events.
322     * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step.
323     * @throws SAXException if an error occurs during the SAX events generation.
324     */
325    protected void saxWorkflowStep(Content content, ContentHandler contentHandler, boolean saxWorkflowInfo) throws SAXException
326    {
327        if (content instanceof WorkflowAwareContent)
328        {
329            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
330            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
331            
332            try
333            {
334                long workflowId = waContent.getWorkflowId();
335                String workflowName = workflow.getWorkflowName(workflowId);
336                
337                Step currentStep = getCurrentStep(waContent, workflow);
338                
339                int currentStepId = currentStep.getStepId();
340                
341                I18nizableText workflowStepName = new I18nizableText("application",  _worklflowHelper.getStepName(workflowName, currentStepId));
342                
343                AttributesImpl atts = new AttributesImpl();
344                atts.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId));
345                atts.addAttribute("", "workflowName", "workflowName", "CDATA", String.valueOf(workflowName));
346                
347                if (saxWorkflowInfo)
348                {
349                    if ("application".equals(workflowStepName.getCatalogue()))
350                    {
351                        atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png");
352                        atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png");
353                        atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png");
354                    }
355                    else
356                    {
357                        String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
358                        atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png");
359                        atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png");
360                        atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png");
361                    }
362                }
363                
364                XMLUtils.startElement(contentHandler, "workflow-step", atts);
365                
366                if (saxWorkflowInfo)
367                {
368                    workflowStepName.toSAX(contentHandler);
369                }
370                
371                XMLUtils.endElement(contentHandler, "workflow-step");
372            }
373            catch (AmetysRepositoryException e)
374            {
375                // Current step id was not positioned
376            }
377            catch (WorkflowException e)
378            {
379                // Ignore, just don't SAX the workflow step.
380            }
381        }
382    }
383
384    /**
385     * Get the current workflow step of the content.
386     * @param content the {@link Content}.
387     * @param workflow the associated workflow.
388     * @return the current step
389     * @throws WorkflowException if somethng got wrong processing workflow data.
390     */
391    protected Step getCurrentStep(WorkflowAwareContent content, AmetysObjectWorkflow workflow) throws WorkflowException
392    {
393        long workflowId = content.getWorkflowId();
394        
395        Step currentStep = (Step) workflow.getCurrentSteps(workflowId).get(0);
396        
397        if (content instanceof VersionAwareAmetysObject)
398        {
399            VersionAwareAmetysObject vaContent = (VersionAwareAmetysObject) content;
400            String currentRevision = vaContent.getRevision();
401            
402            if (currentRevision != null)
403            {
404                
405                String[] allRevisions = vaContent.getAllRevisions();
406                int currentRevIndex = ArrayUtils.indexOf(allRevisions, currentRevision);
407                
408                if (currentRevIndex > -1 && currentRevIndex < (allRevisions.length - 1))
409                {
410                    String nextRevision = allRevisions[currentRevIndex + 1];
411                    
412                    Date currentRevTimestamp = vaContent.getRevisionTimestamp();
413                    Date nextRevTimestamp = vaContent.getRevisionTimestamp(nextRevision);
414                    
415                    // Get all steps between the two revisions. 
416                    List<Step> steps = _worklflowHelper.getStepsBetween(workflow, workflowId, currentRevTimestamp, nextRevTimestamp);
417                    
418                    // In the old workflow structure
419                    // We take the second, which is current revision's last step.
420                    if (steps.size() > 0 && steps.get(0) instanceof AmetysStep)
421                    {
422                        AmetysStep amStep = (AmetysStep) steps.get(0);
423                        if (amStep.getProperty("actionFinishDate") != null)
424                        {
425                            // New workflow structure detected: cut the first workflow step
426                            // in the list, as it belongs to the next version.
427                            steps = steps.subList(1, steps.size());
428                        }
429                    }
430                    
431                    // Order by step descendant.
432                    Collections.sort(steps, new Comparator<Step>()
433                    {
434                        public int compare(Step step1, Step step2)
435                        {
436                            return -Long.valueOf(step1.getId()).compareTo(step2.getId());
437                        }
438                    });
439                    
440                    // The first step in the list is the current version's last workflow step.
441                    if (steps.size() > 0)
442                    {
443                        currentStep = steps.get(0);
444                    }
445                }
446            }
447        }
448        
449        return currentStep;
450    }
451    
452    /**
453     * Generates SAX events for the content's language.
454     * @param content the {@link Content}.
455     * @param contentHandler the ContentHandler receving SAX events.
456     * @throws SAXException if an error occurs during the SAX events generation.
457     */
458    protected void saxLanguage(Content content, ContentHandler contentHandler) throws SAXException
459    {
460        String code = content.getLanguage();
461        if (code != null)
462        {
463            Language language = _languageManager.getLanguage(code);
464            
465            AttributesImpl atts = new AttributesImpl();
466            atts.addCDATAAttribute("code", code);
467            
468            if (language != null)
469            {
470                atts.addCDATAAttribute("icon-small", language.getSmallIcon());
471                atts.addCDATAAttribute("icon-medium", language.getMediumIcon());
472                atts.addCDATAAttribute("icon-large", language.getLargeIcon());
473            }
474            
475            XMLUtils.startElement(contentHandler, "content-language", atts);
476            if (language != null)
477            {
478                language.getLabel().toSAX(contentHandler);
479            }
480            XMLUtils.endElement(contentHandler, "content-language");
481        }
482    }
483    
484    /**
485     * Generates SAX events for the DC metadata.
486     * @param dcObject the {@link Content}.
487     * @param contentHandler the ContentHandler receving SAX events.
488     * @throws SAXException if an error occurs during the SAX events generation.
489     */
490    protected void saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject, ContentHandler contentHandler) throws SAXException
491    {
492        XMLUtils.startElement(contentHandler, "dublin-core-metadata");
493        saxIfNotNull("title", dcObject.getDCTitle(), contentHandler);
494        saxIfNotNull("creator", dcObject.getDCCreator(), contentHandler);
495        saxIfNotNull("subject", dcObject.getDCSubject(), contentHandler);
496        saxIfNotNull("description", dcObject.getDCDescription(), contentHandler);
497        saxIfNotNull("publisher", dcObject.getDCPublisher(), contentHandler);
498        saxIfNotNull("contributor", dcObject.getDCContributor(), contentHandler);
499        saxIfNotNull("date", dcObject.getDCDate(), contentHandler);
500        saxIfNotNull("type", dcObject.getDCType(), contentHandler);
501        saxIfNotNull("format", dcObject.getDCFormat(), contentHandler);
502        saxIfNotNull("identifier", dcObject.getDCIdentifier(), contentHandler);
503        saxIfNotNull("source", dcObject.getDCSource(), contentHandler);
504        saxIfNotNull("language", dcObject.getDCLanguage(), contentHandler);
505        saxIfNotNull("relation", dcObject.getDCRelation(), contentHandler);
506        saxIfNotNull("coverage", dcObject.getDCCoverage(), contentHandler);
507        saxIfNotNull("rights", dcObject.getDCRights(), contentHandler);
508        XMLUtils.endElement(contentHandler, "dublin-core-metadata");
509    }
510
511    /**
512     * Send a value if not null.
513     * @param name the tag name.
514     * @param value the value.
515     * @param contentHandler the ContentHandler receving SAX events.
516     * @throws SAXException if an error occurs during the SAX events generation.
517     */
518    protected void saxIfNotNull(String name, String value, ContentHandler contentHandler) throws SAXException
519    {
520        if (value != null)
521        {
522            XMLUtils.createElement(contentHandler, name, value);
523        }
524    }
525    
526    /**
527     * Send values if not null.
528     * @param name the tag name.
529     * @param values the values.
530     * @param contentHandler the ContentHandler receving SAX events.
531     * @throws SAXException if an error occurs during the SAX events generation.
532     */
533    protected void saxIfNotNull(String name, String[] values, ContentHandler contentHandler) throws SAXException
534    {
535        if (values != null)
536        {
537            for (String value : values)
538            {
539                XMLUtils.createElement(contentHandler, name, value);
540            }
541        }
542    }
543    
544    /**
545     * Send a value if not null.
546     * @param name the tag name.
547     * @param value the value.
548     * @param contentHandler the ContentHandler receving SAX events.
549     * @throws SAXException if an error occurs during the SAX events generation.
550     */
551    protected void saxIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException
552    {
553        if (value != null)
554        {
555            LocalDate ld = DateUtils.asLocalDate(value);
556            XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE));
557        }
558    }
559    
560    /**
561     * Generates SAX events for content's comments.
562     * @param content the {@link Content}.
563     * @param contentHandler the ContentHandler receving SAX events.
564     * @throws SAXException if an error occurs during the SAX events generation.
565     */
566    protected void saxContentComments(CommentableContent content, ContentHandler contentHandler) throws SAXException
567    {
568        List<Comment> comments = content.getComments(false, true);
569        int level = 0;
570        saxComments(comments, "comments", level, contentHandler);
571    }
572    
573    /**
574     * Generates SAX events for comments.
575     * @param comments the comments.
576     * @param elementName the tag name.
577     * @param level the comments level.
578     * @param contentHandler the ContentHandler receving SAX events.
579     * @throws SAXException if an error occurs during the SAX events generation.
580     */
581    protected void saxComments(List<Comment> comments, String elementName, int level, ContentHandler contentHandler) throws SAXException
582    {
583        if (comments.size() > 0)
584        {
585            XMLUtils.startElement(contentHandler, elementName);
586            
587            for (Comment comment : comments)
588            {
589                saxComment(comment, level, contentHandler);
590            }
591            
592            XMLUtils.endElement(contentHandler, elementName);
593        }
594    }
595    
596    /**
597     * Generates SAX events for a single comment.
598     * @param comment the comment.
599     * @param level the comment level.
600     * @param contentHandler the ContentHandler receving SAX events.
601     * @throws SAXException if an error occurs during the SAX events generation.
602     */
603    protected void saxComment(Comment comment, int level, ContentHandler contentHandler) throws SAXException
604    {
605        AttributesImpl attrs = new AttributesImpl();
606
607        attrs.addCDATAAttribute("id", comment.getId());
608        attrs.addCDATAAttribute("creation-date", DateUtils.zonedDateTimeToString(comment.getCreationDate()));
609        attrs.addCDATAAttribute("level", String.valueOf(level));
610        attrs.addCDATAAttribute("is-validated", String.valueOf(comment.isValidated()));
611        attrs.addCDATAAttribute("is-email-hidden", String.valueOf(comment.isEmailHidden()));
612        
613        if (!StringUtils.isBlank(comment.getAuthorName()))
614        {
615            attrs.addCDATAAttribute("author-name", comment.getAuthorName());
616        }
617        
618        if (!comment.isEmailHidden() && !StringUtils.isBlank(comment.getAuthorEmail()))
619        {
620            attrs.addCDATAAttribute("author-email", comment.getAuthorEmail());
621        }
622
623        if (!StringUtils.isBlank(comment.getAuthorURL()))
624        {
625            attrs.addCDATAAttribute("author-url", comment.getAuthorURL());
626        }
627
628        XMLUtils.startElement(contentHandler, "comment", attrs);
629
630        if (comment.getContent() != null)
631        {
632            String[] contents = comment.getContent().split("\r?\n");
633            for (String c : contents)
634            {
635                XMLUtils.createElement(contentHandler, "p", c);
636            }
637        }
638        
639        // The generated SAXed events for the comments' reaction have changed.
640        // In the SAXed events of the ContentGenerator (that should one day use this ContentSaxer):
641        // - there is a "nb-like" attributes on the comment node that disappears here
642        // - reactions are SAXed as a simple list of ("likers")
643        saxReactions(comment, contentHandler);
644        
645        saxReports(comment, contentHandler);
646
647        saxComments(comment.getSubComment(false, true), "sub-comments", level + 1, contentHandler);
648
649        XMLUtils.endElement(contentHandler, "comment");
650    }
651    
652    /**
653     * Generates SAX events for the given object's reactions.
654     * @param reactionable the {@link ReactionableObject}.
655     * @param contentHandler the ContentHandler receiving SAX events.
656     * @throws SAXException if an error occurs during the SAX events generation.
657     */
658    protected void saxReactions(ReactionableObject reactionable, ContentHandler contentHandler) throws SAXException
659    {
660        XMLUtils.startElement(contentHandler, "reactions");
661
662        for (ReactionType reactionType : ReactionType.values())
663        {
664            List<UserIdentity> actors = reactionable.getReactionUsers(reactionType);
665            if (!actors.isEmpty())
666            {
667                AttributesImpl attrs = new AttributesImpl();
668                attrs.addCDATAAttribute("type", reactionType.name());
669                XMLUtils.startElement(contentHandler, "reaction", attrs);
670    
671                for (UserIdentity actor : actors)
672                {
673                    _userHelper.saxUserIdentity(actor, contentHandler, "actor");
674                }
675    
676                XMLUtils.endElement(contentHandler, "reaction");
677            }
678        }
679        XMLUtils.endElement(contentHandler, "reactions");
680    }
681    
682    /**
683     * Generates SAX events for the given object's reports.
684     * @param reportable the {@link Content}.
685     * @param contentHandler the ContentHandler receiving SAX events.
686     * @throws SAXException if an error occurs during the SAX events generation.
687     */
688    protected void saxReports(ReportableObject reportable, ContentHandler contentHandler) throws SAXException
689    {
690        long reportsCount = reportable.getReportsCount();
691        if (reportsCount > 0)
692        {
693            AttributesImpl attrs = new AttributesImpl();
694            attrs.addCDATAAttribute("count", String.valueOf(reportsCount));
695            XMLUtils.createElement(contentHandler, "reports", attrs);
696        }
697    }
698    
699    /**
700     * Fills the given content with the values from the provided {@link org.w3c.dom.Node}.
701     * <br>This is the anti-operation of {@link #saxContent}, as the org.w3c.dom.Node should be a Node previously generated with SAX events from this method.
702     * @param content The content to fill
703     * @param node The node to read for retrieving values to fill
704     * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes
705     * @throws Exception if an exception occurs
706     */
707    public void fillContent(ModifiableContent content, org.w3c.dom.Node node, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception
708    {
709        org.w3c.dom.Node contentNode = XPathAPI.selectSingleNode(node, "content");
710        
711        fillAttributes(content, contentNode, additionalDataGetter);
712        fillDublinCore(content, contentNode);
713        
714        if (content instanceof CommentableContent)
715        {
716            fillContentComments((CommentableContent) content, contentNode);
717        }
718        
719        if (content instanceof ReactionableObject)
720        {
721            fillReactions((ReactionableObject) content, contentNode);
722        }
723        
724        if (content instanceof ReportableObject)
725        {
726            fillReports((ReportableObject) content, contentNode);
727        }
728    }
729    
730    /**
731     * Fills the given object with the dublin core values from the provided {@link org.w3c.dom.Node}.
732     * @param dcObject The object to fill
733     * @param node The node to read to get the values to fill
734     * @throws Exception if an exception occurs
735     */
736    protected void fillDublinCore(ModifiableDublinCoreAwareAmetysObject dcObject, org.w3c.dom.Node node) throws Exception
737    {
738        org.w3c.dom.Node dcNode = XPathAPI.selectSingleNode(node, "dublin-core-metadata");
739        if (dcNode != null)
740        {
741            setIfNotNull(dcNode, "title", XObject::str, dcObject::setDCTitle);
742            setIfNotNull(dcNode, "creator", XObject::str, dcObject::setDCCreator);
743            setIfNotNull(dcNode, "subject", this::values, dcObject::setDCSubject);
744            setIfNotNull(dcNode, "description", XObject::str, dcObject::setDCDescription);
745            setIfNotNull(dcNode, "publisher", XObject::str, dcObject::setDCPublisher);
746            setIfNotNull(dcNode, "contributor", XObject::str, dcObject::setDCContributor);
747            setIfNotNull(dcNode, "date", this::dateValue, dcObject::setDCDate);
748            setIfNotNull(dcNode, "type", XObject::str, dcObject::setDCType);
749            setIfNotNull(dcNode, "format", XObject::str, dcObject::setDCFormat);
750            setIfNotNull(dcNode, "identifier", XObject::str, dcObject::setDCIdentifier);
751            setIfNotNull(dcNode, "source", XObject::str, dcObject::setDCSource);
752            setIfNotNull(dcNode, "language", XObject::str, dcObject::setDCLanguage);
753            setIfNotNull(dcNode, "relation", XObject::str, dcObject::setDCRelation);
754            setIfNotNull(dcNode, "coverage", XObject::str, dcObject::setDCCoverage);
755            setIfNotNull(dcNode, "rights", XObject::str, dcObject::setDCRights);
756        }
757    }
758    
759    /**
760     * Fills the given content with the comments from the provided {@link org.w3c.dom.Node}
761     * @param content The content to fill
762     * @param contentNode the node to read to get the comments' values
763     * @throws Exception if an error occurs
764     */
765    protected void fillContentComments(CommentableContent content, org.w3c.dom.Node contentNode) throws Exception
766    {
767        NodeList commnentsNodes = XPathAPI.selectNodeList(contentNode, "comments/comment");
768        for (int i = 0; i < commnentsNodes.getLength(); i++)
769        {
770            org.w3c.dom.Node commentNode = commnentsNodes.item(i);
771            
772            String commentId = XPathAPI.eval(commentNode, "@id").str();
773            String creationDateAsString = XPathAPI.eval(commentNode, "@creation-date").str();
774            ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString);
775            
776            Comment comment = content.createComment(commentId, creationDate);
777            fillComment(comment, commentNode);
778        }
779    }
780    
781    /**
782     * Fills the given comment with the values from the provided {@link org.w3c.dom.Node}
783     * @param comment The comment to fill
784     * @param commentNode the node to read to get the comment's values
785     * @throws Exception if an error occurs
786     */
787    protected void fillComment(Comment comment, org.w3c.dom.Node commentNode) throws Exception
788    {
789        setIfNotNull(commentNode, "@is-validated", this::booleanValue, comment::setValidated);
790        setIfNotNull(commentNode, "@is-email-hidden", this::booleanValue, comment::setEmailHiddenStatus);
791        setIfNotNull(commentNode, "@author-name", XObject::str, comment::setAuthorName);
792        setIfNotNull(commentNode, "@author-email", XObject::str, comment::setAuthorEmail);
793        setIfNotNull(commentNode, "@author-url", XObject::str, comment::setAuthorURL);
794        
795        StringBuilder content = new StringBuilder();
796        NodeList paragraphs = XPathAPI.selectNodeList(commentNode, "p");
797        for (int i = 0; i < paragraphs.getLength(); i++)
798        {
799            org.w3c.dom.Node paragraph = paragraphs.item(i);
800            content.append(paragraph.getTextContent())
801                .append("\r\n");
802        }
803        comment.setContent(content.toString());
804        
805        fillReactions(comment, commentNode);
806        
807        fillReports(comment, commentNode);
808        
809        NodeList subCommnentsNodes = XPathAPI.selectNodeList(commentNode, "sub-comments/comment");
810        for (int i = 0; i < subCommnentsNodes.getLength(); i++)
811        {
812            org.w3c.dom.Node subCommentNode = subCommnentsNodes.item(i);
813            
814            String subCommentId = XPathAPI.eval(subCommentNode, "@id").str();
815            String creationDateAsString = XPathAPI.eval(subCommentNode, "@creation-date").str();
816            ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString);
817            
818            Comment subComment = comment.createSubComment(subCommentId, creationDate);
819            fillComment(subComment, subCommentNode);
820        }
821    }
822    
823    /**
824     * Fills the given {@link ReactionableObject} with the reactions from the provided {@link org.w3c.dom.Node}
825     * @param reactionable The {@link ReactionableObject} to fill
826     * @param node the node to read to get the reactions
827     * @throws Exception if an error occurs
828     */
829    protected void fillReactions(ReactionableObject reactionable, org.w3c.dom.Node node) throws Exception
830    {
831        NodeList reactions = XPathAPI.selectNodeList(node, "reactions/reaction");
832        for (int i = 0; i < reactions.getLength(); i++)
833        {
834            org.w3c.dom.Node reactionNode = reactions.item(i);
835            XObject reactionTypeAttr = XPathAPI.eval(reactionNode, "@type");
836            ReactionType reactionType = ReactionType.valueOf(reactionTypeAttr.str());
837            NodeList actors = XPathAPI.selectNodeList(reactionNode, "actor");
838            for (int j = 0; j < actors.getLength(); j++)
839            {
840                org.w3c.dom.Node actorNode = actors.item(j);
841                UserIdentity actor = _userHelper.xml2userIdentity(actorNode);
842                reactionable.addReaction(actor, reactionType);
843            }
844        }
845    }
846    
847    /**
848     * Fills the given {@link ReportableObject} with the reports from the provided {@link org.w3c.dom.Node}
849     * @param reportable The {@link ReportableObject} to fill
850     * @param node the node to read to get the reports
851     * @throws Exception if an error occurs
852     */
853    protected void fillReports(ReportableObject reportable, org.w3c.dom.Node node) throws Exception
854    {
855        org.w3c.dom.Node reportsNode = XPathAPI.selectSingleNode(node, "reports");
856        if (reportsNode != null)
857        {
858            XObject reportsCountAttr = XPathAPI.eval(reportsNode, "@count");
859            long reportsCount = (long) reportsCountAttr.num();
860            
861            if (reportsCount > 0)
862            {
863                reportable.setReportsCount(reportsCount);
864            }
865        }
866    }
867    
868    /**
869     * Fills the given content with the attributes from the provided {@link org.w3c.dom.Node}
870     * @param content The content to fill
871     * @param contentNode the node to read to get the attributes
872     * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes
873     * @throws Exception if an error occurs
874     */
875    protected void fillAttributes(ModifiableContent content, org.w3c.dom.Node contentNode, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception
876    {
877        Element attributesElement = (Element) XPathAPI.selectSingleNode(contentNode, "attributes");
878        @SuppressWarnings("unchecked")
879        Collection<Model> contentModels = (Collection<Model>) content.getModel();
880        View view = View.of(contentModels);
881        Map<String, Object> values = new ModelAwareXMLValuesExtractor(attributesElement, additionalDataGetter, contentModels)
882                .extractValues(view);
883        
884        content.synchronizeValues(view, values);
885    }
886    
887    /**
888     * Sets a value through the given setter if the value is not null
889     * @param <T> The type of the value to set
890     * @param node The node to read to get the value
891     * @param expression The expression to apply on the node to get the value
892     * @param retriever The {@link Function} that will retrieve the typed value from the {@link XObject} evaluated from the expression
893     * @param setter The {@link Consumer} that will be used to set the value 
894     * @throws Exception if an error occurs
895     */
896    protected <T> void setIfNotNull(org.w3c.dom.Node node, String expression, ThrowingFunction<XObject, T> retriever, Consumer<T> setter) throws Exception
897    {
898        XObject xObject = XPathAPI.eval(node, expression);
899        if (xObject.getType() != XObject.CLASS_NULL)
900        {
901            T value = retriever.apply(xObject);
902            if (value != null)
903            {
904                setter.accept(value);
905            }
906        }
907    }
908    
909    /**
910     * Consumes an {@link XObject} to retrieve the value as a {@link Date}
911     * @param xObject The consumed {@link XObject}
912     * @return The {@link Date} value
913     */
914    protected Date dateValue(XObject xObject)
915    {
916        String dateAsString = xObject.str();
917        if (StringUtils.isBlank(dateAsString))
918        {
919            return null;
920        }
921        LocalDate localDate = DateTimeFormatter.ISO_LOCAL_DATE.parse(dateAsString, LocalDate::from);
922        return DateUtils.asDate(localDate);
923    }
924    
925    /**
926     * Consumes an {@link XObject} to retrieve the value as a {@link Boolean}
927     * @param xObject The consumed {@link XObject}
928     * @return The {@link Boolean} value
929     */
930    protected Boolean booleanValue(XObject xObject)
931    {
932        String booleanAsString = xObject.str();
933        return Boolean.valueOf(booleanAsString);
934    }
935    
936    /**
937     * Consumes an {@link XObject} to retrieve the value as a String array
938     * @param xObject The consumed {@link XObject}
939     * @return The String array
940     * @throws TransformerException if an error occurs
941     */
942    protected String[] values(XObject xObject) throws TransformerException
943    {
944        NodeList nodeList = xObject.nodelist();
945        return IntStream.range(0, nodeList.getLength())
946                .mapToObj(nodeList::item)
947                .map(org.w3c.dom.Node::getTextContent)
948                .toArray(String[]::new);
949    }
950}