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