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}