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