001/*
002 *  Copyright 2020 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.Collection;
022import java.util.Date;
023import java.util.Map;
024import java.util.function.BiFunction;
025import java.util.function.Consumer;
026import java.util.function.Function;
027import java.util.stream.IntStream;
028
029import javax.xml.transform.TransformerException;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.xpath.XPathAPI;
037import org.apache.xpath.objects.XObject;
038import org.w3c.dom.Element;
039import org.w3c.dom.NodeList;
040
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.ModifiableContent;
043import org.ametys.cms.repository.ReactionableObject;
044import org.ametys.cms.repository.ReportableObject;
045import org.ametys.cms.repository.ReactionableObject.ReactionType;
046import org.ametys.cms.repository.comment.Comment;
047import org.ametys.cms.repository.comment.CommentableContent;
048import org.ametys.cms.repository.comment.contributor.ContributorCommentableContent;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.core.util.DateUtils;
051import org.ametys.core.util.LambdaUtils.ThrowingFunction;
052import org.ametys.plugins.core.user.UserHelper;
053import org.ametys.plugins.repository.data.extractor.xml.ModelAwareXMLValuesExtractor;
054import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter;
055import org.ametys.plugins.repository.dublincore.ModifiableDublinCoreAwareAmetysObject;
056import org.ametys.runtime.model.Model;
057import org.ametys.runtime.model.View;
058import org.ametys.runtime.model.ViewHelper;
059
060/**
061 * Component responsible to extract a {@link Content} from an XML document.
062 */
063public class ContentExtractor implements Component, Serviceable
064{
065    /** Avalon role. */
066    public static final String CMS_CONTENT_EXTACTOR_ROLE = ContentExtractor.class.getName();
067    
068    private UserHelper _userHelper;
069
070    public void service(ServiceManager manager) throws ServiceException
071    {
072        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
073    }
074    
075    /**
076     * Fills the given content with the values from the provided {@link org.w3c.dom.Node}.
077     * <br>This is the anti-operation of {@link ContentSaxer#saxContent}, as the org.w3c.dom.Node should be a Node previously generated with SAX events from this method.
078     * @param content The content to fill
079     * @param node The node to read for retrieving values to fill
080     * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes
081     * @throws Exception if an exception occurs
082     */
083    public void fillContent(ModifiableContent content, org.w3c.dom.Node node, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception
084    {
085        org.w3c.dom.Node contentNode = XPathAPI.selectSingleNode(node, "content");
086        
087        fillAttributes(content, contentNode, additionalDataGetter);
088        fillDublinCore(content, contentNode);
089        
090        if (content instanceof CommentableContent commentableContent)
091        {
092            fillContentComments(contentNode, "comments", commentableContent::createComment);
093        }
094        
095        if (content instanceof ContributorCommentableContent contributorCommentableContent)
096        {
097            fillContentComments(contentNode, "contributor-comments", contributorCommentableContent::createContributorComment);
098        }
099        
100        if (content instanceof ReactionableObject reactionableObject)
101        {
102            fillReactions(reactionableObject, contentNode);
103        }
104        
105        if (content instanceof ReportableObject reportableObject)
106        {
107            fillReports(reportableObject, contentNode);
108        }
109    }
110    
111    /**
112     * Fills the given object with the dublin core values from the provided {@link org.w3c.dom.Node}.
113     * @param dcObject The object to fill
114     * @param node The node to read to get the values to fill
115     * @throws Exception if an exception occurs
116     */
117    protected void fillDublinCore(ModifiableDublinCoreAwareAmetysObject dcObject, org.w3c.dom.Node node) throws Exception
118    {
119        org.w3c.dom.Node dcNode = XPathAPI.selectSingleNode(node, "dublin-core-metadata");
120        if (dcNode != null)
121        {
122            setIfNotNull(dcNode, "title", XObject::str, dcObject::setDCTitle);
123            setIfNotNull(dcNode, "creator", XObject::str, dcObject::setDCCreator);
124            setIfNotNull(dcNode, "subject", this::values, dcObject::setDCSubject);
125            setIfNotNull(dcNode, "description", XObject::str, dcObject::setDCDescription);
126            setIfNotNull(dcNode, "publisher", XObject::str, dcObject::setDCPublisher);
127            setIfNotNull(dcNode, "contributor", XObject::str, dcObject::setDCContributor);
128            setIfNotNull(dcNode, "date", this::dateValue, dcObject::setDCDate);
129            setIfNotNull(dcNode, "type", XObject::str, dcObject::setDCType);
130            setIfNotNull(dcNode, "format", XObject::str, dcObject::setDCFormat);
131            setIfNotNull(dcNode, "identifier", XObject::str, dcObject::setDCIdentifier);
132            setIfNotNull(dcNode, "source", XObject::str, dcObject::setDCSource);
133            setIfNotNull(dcNode, "language", XObject::str, dcObject::setDCLanguage);
134            setIfNotNull(dcNode, "relation", XObject::str, dcObject::setDCRelation);
135            setIfNotNull(dcNode, "coverage", XObject::str, dcObject::setDCCoverage);
136            setIfNotNull(dcNode, "rights", XObject::str, dcObject::setDCRights);
137        }
138    }
139    
140    /**
141     * Fills the content with the comments from the provided {@link org.w3c.dom.Node}
142     * @param contentNode the node to read to get the comments' values
143     * @param commentsNodesName the name of comments nodes
144     * @param createCommentFunction the function to use to create comments on the content
145     * @throws Exception if an error occurs
146     */
147    protected void fillContentComments(org.w3c.dom.Node contentNode, String commentsNodesName, BiFunction<String, ZonedDateTime, Comment> createCommentFunction) throws Exception
148    {
149        NodeList commnentsNodes = XPathAPI.selectNodeList(contentNode, commentsNodesName + "/comment");
150        for (int i = 0; i < commnentsNodes.getLength(); i++)
151        {
152            org.w3c.dom.Node commentNode = commnentsNodes.item(i);
153            
154            String commentId = XPathAPI.eval(commentNode, "@id").str();
155            String creationDateAsString = XPathAPI.eval(commentNode, "@creation-date").str();
156            ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString);
157            
158            Comment comment = createCommentFunction.apply(commentId, creationDate);
159            fillComment(comment, commentNode);
160        }
161    }
162    
163    /**
164     * Fills the given comment with the values from the provided {@link org.w3c.dom.Node}
165     * @param comment The comment to fill
166     * @param commentNode the node to read to get the comment's values
167     * @throws Exception if an error occurs
168     */
169    protected void fillComment(Comment comment, org.w3c.dom.Node commentNode) throws Exception
170    {
171        setIfNotNull(commentNode, "@is-validated", this::booleanValue, comment::setValidated);
172        setIfNotNull(commentNode, "@is-email-hidden", this::booleanValue, comment::setEmailHiddenStatus);
173        setIfNotNull(commentNode, "@author-name", XObject::str, comment::setAuthorName);
174        setIfNotNull(commentNode, "@author-email", XObject::str, comment::setAuthorEmail);
175        setIfNotNull(commentNode, "@author-url", XObject::str, comment::setAuthorURL);
176        
177        org.w3c.dom.Node authorNode = XPathAPI.selectSingleNode(commentNode, "author");
178        if (authorNode != null)
179        {
180            UserIdentity author = _userHelper.xml2userIdentity(authorNode);
181            comment.setAuthor(author);
182        }
183        
184        StringBuilder content = new StringBuilder();
185        NodeList paragraphs = XPathAPI.selectNodeList(commentNode, "p");
186        for (int i = 0; i < paragraphs.getLength(); i++)
187        {
188            org.w3c.dom.Node paragraph = paragraphs.item(i);
189            content.append(paragraph.getTextContent())
190                .append("\r\n");
191        }
192        comment.setContent(content.toString());
193        
194        fillReactions(comment, commentNode);
195        
196        fillReports(comment, commentNode);
197        
198        NodeList subCommnentsNodes = XPathAPI.selectNodeList(commentNode, "sub-comments/comment");
199        for (int i = 0; i < subCommnentsNodes.getLength(); i++)
200        {
201            org.w3c.dom.Node subCommentNode = subCommnentsNodes.item(i);
202            
203            String subCommentId = XPathAPI.eval(subCommentNode, "@id").str();
204            String creationDateAsString = XPathAPI.eval(subCommentNode, "@creation-date").str();
205            ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString);
206            
207            Comment subComment = comment.createSubComment(subCommentId, creationDate);
208            fillComment(subComment, subCommentNode);
209        }
210    }
211    
212    /**
213     * Fills the given {@link ReactionableObject} with the reactions from the provided {@link org.w3c.dom.Node}
214     * @param reactionable The {@link ReactionableObject} to fill
215     * @param node the node to read to get the reactions
216     * @throws Exception if an error occurs
217     */
218    protected void fillReactions(ReactionableObject reactionable, org.w3c.dom.Node node) throws Exception
219    {
220        NodeList reactions = XPathAPI.selectNodeList(node, "reactions/reaction");
221        for (int i = 0; i < reactions.getLength(); i++)
222        {
223            org.w3c.dom.Node reactionNode = reactions.item(i);
224            XObject reactionTypeAttr = XPathAPI.eval(reactionNode, "@type");
225            ReactionType reactionType = ReactionType.valueOf(reactionTypeAttr.str());
226            NodeList actors = XPathAPI.selectNodeList(reactionNode, "actor");
227            for (int j = 0; j < actors.getLength(); j++)
228            {
229                org.w3c.dom.Node actorNode = actors.item(j);
230                UserIdentity actor = _userHelper.xml2userIdentity(actorNode);
231                reactionable.addReaction(actor, reactionType);
232            }
233        }
234    }
235    
236    /**
237     * Fills the given {@link ReportableObject} with the reports from the provided {@link org.w3c.dom.Node}
238     * @param reportable The {@link ReportableObject} to fill
239     * @param node the node to read to get the reports
240     * @throws Exception if an error occurs
241     */
242    protected void fillReports(ReportableObject reportable, org.w3c.dom.Node node) throws Exception
243    {
244        org.w3c.dom.Node reportsNode = XPathAPI.selectSingleNode(node, "reports");
245        if (reportsNode != null)
246        {
247            XObject reportsCountAttr = XPathAPI.eval(reportsNode, "@count");
248            long reportsCount = (long) reportsCountAttr.num();
249            
250            if (reportsCount > 0)
251            {
252                reportable.setReportsCount(reportsCount);
253            }
254        }
255    }
256    
257    /**
258     * Fills the given content with the attributes from the provided {@link org.w3c.dom.Node}
259     * @param content The content to fill
260     * @param contentNode the node to read to get the attributes
261     * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes
262     * @throws Exception if an error occurs
263     */
264    protected void fillAttributes(ModifiableContent content, org.w3c.dom.Node contentNode, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception
265    {
266        Element attributesElement = (Element) XPathAPI.selectSingleNode(contentNode, "attributes");
267        @SuppressWarnings("unchecked")
268        Collection<Model> contentModels = (Collection<Model>) content.getModel();
269        View view = View.of(contentModels);
270        View truncatedView = ViewHelper.getTruncatedView(view);
271        Map<String, Object> values = new ModelAwareXMLValuesExtractor(attributesElement, additionalDataGetter, contentModels)
272                .extractValues(truncatedView);
273        
274        content.synchronizeValues(truncatedView, values);
275    }
276
277    /**
278     * Sets a value through the given setter if the value is not null
279     * @param <T> The type of the value to set
280     * @param node The node to read to get the value
281     * @param expression The expression to apply on the node to get the value
282     * @param retriever The {@link Function} that will retrieve the typed value from the {@link XObject} evaluated from the expression
283     * @param setter The {@link Consumer} that will be used to set the value 
284     * @throws Exception if an error occurs
285     */
286    protected <T> void setIfNotNull(org.w3c.dom.Node node, String expression, ThrowingFunction<XObject, T> retriever, Consumer<T> setter) throws Exception
287    {
288        XObject xObject = XPathAPI.eval(node, expression);
289        if (xObject.getType() != XObject.CLASS_NULL)
290        {
291            T value = retriever.apply(xObject);
292            if (value != null)
293            {
294                setter.accept(value);
295            }
296        }
297    }
298    
299    /**
300     * Consumes an {@link XObject} to retrieve the value as a {@link Date}
301     * @param xObject The consumed {@link XObject}
302     * @return The {@link Date} value
303     */
304    protected Date dateValue(XObject xObject)
305    {
306        String dateAsString = xObject.str();
307        if (StringUtils.isBlank(dateAsString))
308        {
309            return null;
310        }
311        LocalDate localDate = DateTimeFormatter.ISO_LOCAL_DATE.parse(dateAsString, LocalDate::from);
312        return DateUtils.asDate(localDate);
313    }
314    
315    /**
316     * Consumes an {@link XObject} to retrieve the value as a {@link Boolean}
317     * @param xObject The consumed {@link XObject}
318     * @return The {@link Boolean} value
319     */
320    protected Boolean booleanValue(XObject xObject)
321    {
322        String booleanAsString = xObject.str();
323        return Boolean.valueOf(booleanAsString);
324    }
325    
326    /**
327     * Consumes an {@link XObject} to retrieve the value as a String array
328     * @param xObject The consumed {@link XObject}
329     * @return The String array
330     * @throws TransformerException if an error occurs
331     */
332    protected String[] values(XObject xObject) throws TransformerException
333    {
334        NodeList nodeList = xObject.nodelist();
335        return IntStream.range(0, nodeList.getLength())
336                .mapToObj(nodeList::item)
337                .map(org.w3c.dom.Node::getTextContent)
338                .toArray(String[]::new);
339    }
340}