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}