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}