001/* 002 * Copyright 2019 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.io.IOException; 019import java.time.LocalDate; 020import java.time.ZonedDateTime; 021import java.time.format.DateTimeFormatter; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Date; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.function.Consumer; 030import java.util.function.Function; 031import java.util.stream.IntStream; 032 033import javax.jcr.Node; 034import javax.jcr.RepositoryException; 035import javax.xml.transform.TransformerException; 036 037import org.apache.avalon.framework.component.Component; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.xml.AttributesImpl; 042import org.apache.cocoon.xml.XMLUtils; 043import org.apache.commons.lang.ArrayUtils; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.xpath.XPathAPI; 046import org.apache.xpath.objects.XObject; 047import org.w3c.dom.Element; 048import org.w3c.dom.NodeList; 049import org.xml.sax.ContentHandler; 050import org.xml.sax.SAXException; 051 052import org.ametys.cms.languages.Language; 053import org.ametys.cms.languages.LanguagesManager; 054import org.ametys.cms.repository.Content; 055import org.ametys.cms.repository.ModifiableContent; 056import org.ametys.cms.repository.ReactionableObject; 057import org.ametys.cms.repository.ReactionableObject.ReactionType; 058import org.ametys.cms.repository.ReportableObject; 059import org.ametys.cms.repository.WorkflowAwareContent; 060import org.ametys.cms.repository.comment.Comment; 061import org.ametys.cms.repository.comment.CommentableContent; 062import org.ametys.core.user.UserIdentity; 063import org.ametys.core.util.DateUtils; 064import org.ametys.core.util.LambdaUtils.ThrowingFunction; 065import org.ametys.plugins.core.user.UserHelper; 066import org.ametys.plugins.repository.AmetysRepositoryException; 067import org.ametys.plugins.repository.data.extractor.xml.ModelAwareXMLValuesExtractor; 068import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter; 069import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject; 070import org.ametys.plugins.repository.dublincore.ModifiableDublinCoreAwareAmetysObject; 071import org.ametys.plugins.repository.jcr.JCRAmetysObject; 072import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 073import org.ametys.plugins.workflow.store.AmetysStep; 074import org.ametys.plugins.workflow.support.WorkflowHelper; 075import org.ametys.plugins.workflow.support.WorkflowProvider; 076import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 077import org.ametys.runtime.i18n.I18nizableText; 078import org.ametys.runtime.model.Model; 079import org.ametys.runtime.model.View; 080 081import com.opensymphony.workflow.WorkflowException; 082import com.opensymphony.workflow.spi.Step; 083 084/** 085 * Component responsible for generating SAX events representing a {@link Content}. 086 */ 087public class ContentSaxer implements Serviceable, Component 088{ 089 /** Avalon role. */ 090 public static final String ROLE = ContentSaxer.class.getName(); 091 092 private WorkflowProvider _workflowProvider; 093 private WorkflowHelper _worklflowHelper; 094 private LanguagesManager _languageManager; 095 private UserHelper _userHelper; 096 097 @Override 098 public void service(ServiceManager manager) throws ServiceException 099 { 100 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 101 _worklflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE); 102 _languageManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 103 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 104 } 105 106 /** 107 * Generates SAX events representing a {@link Content}. 108 * <br>When called with a non null tag name, a surrounding element will be generated, 109 * along with XML attributes representing the content's metadata (creation/modification/validation dates and authors, ...). 110 * @param content the {@link Content}. 111 * @param contentHandler the ContentHandler receving SAX events. 112 * @param locale the {@link Locale} to use for eg. multilingual attributes. 113 * @param view the View or null to select all attributes. 114 * @param tagName the surrounding tag name or null to SAX events without root tag. 115 * @param saxWorkflowStep if true, also produces SAX events for the current workflow step. 116 * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step. 117 * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language. 118 * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes. 119 * @throws SAXException if an error occurs during the SAX events generation. 120 */ 121 public void saxContent(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName) throws SAXException 122 { 123 if (StringUtils.isNotEmpty(tagName)) 124 { 125 saxRootTag(content, contentHandler, locale, tagName); 126 } 127 128 saxBody(content, contentHandler, locale, view, tagName, saxWorkflowStep, saxWorkflowInfo, saxLanguageInfo, attributesTagName); 129 130 if (StringUtils.isNotEmpty(tagName)) 131 { 132 XMLUtils.endElement(contentHandler, tagName); 133 } 134 } 135 136 /** 137 * Generates SAX events for the content data. 138 * @param content the {@link Content}. 139 * @param contentHandler the ContentHandler receving SAX events. 140 * @param locale the {@link Locale} to use for eg. multilingual attributes. 141 * @param view the View or null to select all attributes. 142 * @param tagName the surrounding tag name or null to SAX events without root tag. 143 * @param saxWorkflowStep if true, also produces SAX events for the current workflow step. 144 * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step. 145 * @param saxLanguageInfo if true, also produces SAX events for detailed information about the content language. 146 * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes. 147 * @throws SAXException if an error occurs during the SAX events generation. 148 */ 149 protected void saxBody(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, boolean saxWorkflowStep, boolean saxWorkflowInfo, boolean saxLanguageInfo, String attributesTagName) throws SAXException 150 { 151 saxContentTypes(content, contentHandler, true); 152 saxAttributes(content, contentHandler, locale, view, tagName, attributesTagName); 153 154 if (saxWorkflowStep || saxWorkflowInfo) 155 { 156 saxWorkflowStep(content, contentHandler, saxWorkflowInfo); 157 } 158 159 if (saxLanguageInfo) 160 { 161 saxLanguage(content, contentHandler); 162 } 163 164 saxDublinCoreMetadata(content, contentHandler); 165 166 if (content instanceof CommentableContent) 167 { 168 saxContentComments((CommentableContent) content, contentHandler); 169 } 170 171 if (content instanceof ReactionableObject) 172 { 173 saxReactions((ReactionableObject) content, contentHandler); 174 } 175 176 if (content instanceof ReportableObject) 177 { 178 saxReports((ReportableObject) content, contentHandler); 179 } 180 } 181 182 /** 183 * Generates a surrounding tag, with content metadata. 184 * @param content the {@link Content}. 185 * @param contentHandler the ContentHandler receving SAX events. 186 * @param locale the {@link Locale} to use for eg. multilingual attributes. 187 * @param tagName the surrounding tag name or null to SAX events without root tag. 188 * @throws SAXException if an error occurs during the SAX events generation. 189 */ 190 protected void saxRootTag(Content content, ContentHandler contentHandler, Locale locale, String tagName) throws SAXException 191 { 192 AttributesImpl attrs = new AttributesImpl(); 193 attrs.addCDATAAttribute("id", content.getId()); 194 195 if (content instanceof JCRAmetysObject) 196 { 197 _addJcrAttributes((JCRAmetysObject) content, attrs); 198 } 199 200 attrs.addCDATAAttribute("id", content.getId()); 201 attrs.addCDATAAttribute("name", content.getName()); 202 attrs.addCDATAAttribute("title", content.getTitle(locale)); 203 if (content.getLanguage() != null) 204 { 205 attrs.addCDATAAttribute("language", content.getLanguage()); 206 } 207 attrs.addCDATAAttribute("createdAt", DateUtils.dateToString(content.getCreationDate())); 208 attrs.addCDATAAttribute("creator", UserIdentity.userIdentityToString(content.getCreator())); 209 attrs.addCDATAAttribute("lastModifiedAt", DateUtils.dateToString(content.getLastModified())); 210 211 Date lastValidatedAt = content.getLastValidationDate(); 212 if (lastValidatedAt != null) 213 { 214 attrs.addCDATAAttribute("lastValidatedAt", DateUtils.dateToString(lastValidatedAt)); 215 } 216 217 attrs.addCDATAAttribute("lastContributor", UserIdentity.userIdentityToString(content.getLastContributor())); 218 attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent)); 219 220 XMLUtils.startElement(contentHandler, tagName, attrs); 221 } 222 223 private void _addJcrAttributes(JCRAmetysObject content, AttributesImpl attrs) 224 { 225 Node node = content.getNode(); 226 try 227 { 228 attrs.addCDATAAttribute("uuid", node.getIdentifier()); 229 } 230 catch (RepositoryException e) 231 { 232 throw new IllegalArgumentException("Unable to get jcr UUID for content '" + content.getId() + "'", e); 233 } 234 235 try 236 { 237 attrs.addCDATAAttribute("primaryType", node.getPrimaryNodeType().getName()); 238 } 239 catch (RepositoryException e) 240 { 241 throw new IllegalArgumentException("Unable to get jcr Primary Type for content '" + content.getId() + "'", e); 242 } 243 } 244 245 /** 246 * Generates SAX events for {@link Content#getTypes content types}, and possibly {@link Content#getMixinTypes mixin types} 247 * @param content the {@link Content}. 248 * @param contentHandler the ContentHandler receving SAX events. 249 * @param saxMixins if true, also produces SAX events for {@link Content#getMixinTypes mixin types}. 250 * @throws SAXException if an error occurs during the SAX events generation. 251 */ 252 protected void saxContentTypes(Content content, ContentHandler contentHandler, boolean saxMixins) throws SAXException 253 { 254 _saxContentTypes(content, contentHandler); 255 if (saxMixins) 256 { 257 _saxMixins(content, contentHandler); 258 } 259 } 260 261 private void _saxContentTypes(Content content, ContentHandler contentHandler) throws SAXException 262 { 263 String contentTypesTagName = "contentTypes"; 264 String singleContentTypeTagName = "contentType"; 265 XMLUtils.startElement(contentHandler, contentTypesTagName); 266 for (String contentType : content.getTypes()) 267 { 268 XMLUtils.createElement(contentHandler, singleContentTypeTagName, contentType); 269 } 270 XMLUtils.endElement(contentHandler, contentTypesTagName); 271 } 272 273 private void _saxMixins(Content content, ContentHandler contentHandler) throws SAXException 274 { 275 String mixinsTagName = "mixins"; 276 String singleMixinTagName = "mixin"; 277 XMLUtils.startElement(contentHandler, mixinsTagName); 278 for (String mixinType : content.getMixinTypes()) 279 { 280 XMLUtils.createElement(contentHandler, singleMixinTagName, mixinType); 281 } 282 XMLUtils.endElement(contentHandler, mixinsTagName); 283 } 284 285 /** 286 * Generates SAX events for actual content's data. 287 * @param content the {@link Content}. 288 * @param contentHandler the ContentHandler receving SAX events. 289 * @param locale the {@link Locale} to use for eg. multilingual attributes. 290 * @param view the View or null to select all attributes. 291 * @param tagName the surrounding tag name or null to SAX events without root tag. 292 * @param attributesTagName the name of the tag surrounding attributes. Used for legacy purposes. 293 * @throws SAXException if an error occurs during the SAX events generation. 294 */ 295 protected void saxAttributes(Content content, ContentHandler contentHandler, Locale locale, View view, String tagName, String attributesTagName) throws SAXException 296 { 297 try 298 { 299 XMLUtils.startElement(contentHandler, attributesTagName); 300 301 if (view == null) 302 { 303 content.dataToSAX(contentHandler, locale); 304 } 305 else 306 { 307 content.dataToSAX(contentHandler, view, locale); 308 } 309 310 XMLUtils.endElement(contentHandler, attributesTagName); 311 } 312 catch (IOException ex) 313 { 314 throw new RuntimeException(ex); 315 } 316 } 317 318 /** 319 * Generates SAX events representing the current workflow step. 320 * @param content the {@link Content}. 321 * @param contentHandler the ContentHandler receving SAX events. 322 * @param saxWorkflowInfo if true, also produces SAX events for detailed information about the current workflow step. 323 * @throws SAXException if an error occurs during the SAX events generation. 324 */ 325 protected void saxWorkflowStep(Content content, ContentHandler contentHandler, boolean saxWorkflowInfo) throws SAXException 326 { 327 if (content instanceof WorkflowAwareContent) 328 { 329 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 330 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 331 332 try 333 { 334 long workflowId = waContent.getWorkflowId(); 335 String workflowName = workflow.getWorkflowName(workflowId); 336 337 Step currentStep = getCurrentStep(waContent, workflow); 338 339 int currentStepId = currentStep.getStepId(); 340 341 I18nizableText workflowStepName = new I18nizableText("application", _worklflowHelper.getStepName(workflowName, currentStepId)); 342 343 AttributesImpl atts = new AttributesImpl(); 344 atts.addAttribute("", "id", "id", "CDATA", String.valueOf(currentStepId)); 345 atts.addAttribute("", "workflowName", "workflowName", "CDATA", String.valueOf(workflowName)); 346 347 if (saxWorkflowInfo) 348 { 349 if ("application".equals(workflowStepName.getCatalogue())) 350 { 351 atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png"); 352 atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png"); 353 atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png"); 354 } 355 else 356 { 357 String pluginName = workflowStepName.getCatalogue().substring("plugin.".length()); 358 atts.addAttribute("", "icon-small", "icon-small", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png"); 359 atts.addAttribute("", "icon-medium", "icon-medium", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png"); 360 atts.addAttribute("", "icon-large", "icon-large", "CDATA", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png"); 361 } 362 } 363 364 XMLUtils.startElement(contentHandler, "workflow-step", atts); 365 366 if (saxWorkflowInfo) 367 { 368 workflowStepName.toSAX(contentHandler); 369 } 370 371 XMLUtils.endElement(contentHandler, "workflow-step"); 372 } 373 catch (AmetysRepositoryException e) 374 { 375 // Current step id was not positioned 376 } 377 catch (WorkflowException e) 378 { 379 // Ignore, just don't SAX the workflow step. 380 } 381 } 382 } 383 384 /** 385 * Get the current workflow step of the content. 386 * @param content the {@link Content}. 387 * @param workflow the associated workflow. 388 * @return the current step 389 * @throws WorkflowException if somethng got wrong processing workflow data. 390 */ 391 protected Step getCurrentStep(WorkflowAwareContent content, AmetysObjectWorkflow workflow) throws WorkflowException 392 { 393 long workflowId = content.getWorkflowId(); 394 395 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).get(0); 396 397 if (content instanceof VersionAwareAmetysObject) 398 { 399 VersionAwareAmetysObject vaContent = (VersionAwareAmetysObject) content; 400 String currentRevision = vaContent.getRevision(); 401 402 if (currentRevision != null) 403 { 404 405 String[] allRevisions = vaContent.getAllRevisions(); 406 int currentRevIndex = ArrayUtils.indexOf(allRevisions, currentRevision); 407 408 if (currentRevIndex > -1 && currentRevIndex < (allRevisions.length - 1)) 409 { 410 String nextRevision = allRevisions[currentRevIndex + 1]; 411 412 Date currentRevTimestamp = vaContent.getRevisionTimestamp(); 413 Date nextRevTimestamp = vaContent.getRevisionTimestamp(nextRevision); 414 415 // Get all steps between the two revisions. 416 List<Step> steps = _worklflowHelper.getStepsBetween(workflow, workflowId, currentRevTimestamp, nextRevTimestamp); 417 418 // In the old workflow structure 419 // We take the second, which is current revision's last step. 420 if (steps.size() > 0 && steps.get(0) instanceof AmetysStep) 421 { 422 AmetysStep amStep = (AmetysStep) steps.get(0); 423 if (amStep.getProperty("actionFinishDate") != null) 424 { 425 // New workflow structure detected: cut the first workflow step 426 // in the list, as it belongs to the next version. 427 steps = steps.subList(1, steps.size()); 428 } 429 } 430 431 // Order by step descendant. 432 Collections.sort(steps, new Comparator<Step>() 433 { 434 public int compare(Step step1, Step step2) 435 { 436 return -Long.valueOf(step1.getId()).compareTo(step2.getId()); 437 } 438 }); 439 440 // The first step in the list is the current version's last workflow step. 441 if (steps.size() > 0) 442 { 443 currentStep = steps.get(0); 444 } 445 } 446 } 447 } 448 449 return currentStep; 450 } 451 452 /** 453 * Generates SAX events for the content's language. 454 * @param content the {@link Content}. 455 * @param contentHandler the ContentHandler receving SAX events. 456 * @throws SAXException if an error occurs during the SAX events generation. 457 */ 458 protected void saxLanguage(Content content, ContentHandler contentHandler) throws SAXException 459 { 460 String code = content.getLanguage(); 461 if (code != null) 462 { 463 Language language = _languageManager.getLanguage(code); 464 465 AttributesImpl atts = new AttributesImpl(); 466 atts.addCDATAAttribute("code", code); 467 468 if (language != null) 469 { 470 atts.addCDATAAttribute("icon-small", language.getSmallIcon()); 471 atts.addCDATAAttribute("icon-medium", language.getMediumIcon()); 472 atts.addCDATAAttribute("icon-large", language.getLargeIcon()); 473 } 474 475 XMLUtils.startElement(contentHandler, "content-language", atts); 476 if (language != null) 477 { 478 language.getLabel().toSAX(contentHandler); 479 } 480 XMLUtils.endElement(contentHandler, "content-language"); 481 } 482 } 483 484 /** 485 * Generates SAX events for the DC metadata. 486 * @param dcObject the {@link Content}. 487 * @param contentHandler the ContentHandler receving SAX events. 488 * @throws SAXException if an error occurs during the SAX events generation. 489 */ 490 protected void saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject, ContentHandler contentHandler) throws SAXException 491 { 492 XMLUtils.startElement(contentHandler, "dublin-core-metadata"); 493 saxIfNotNull("title", dcObject.getDCTitle(), contentHandler); 494 saxIfNotNull("creator", dcObject.getDCCreator(), contentHandler); 495 saxIfNotNull("subject", dcObject.getDCSubject(), contentHandler); 496 saxIfNotNull("description", dcObject.getDCDescription(), contentHandler); 497 saxIfNotNull("publisher", dcObject.getDCPublisher(), contentHandler); 498 saxIfNotNull("contributor", dcObject.getDCContributor(), contentHandler); 499 saxIfNotNull("date", dcObject.getDCDate(), contentHandler); 500 saxIfNotNull("type", dcObject.getDCType(), contentHandler); 501 saxIfNotNull("format", dcObject.getDCFormat(), contentHandler); 502 saxIfNotNull("identifier", dcObject.getDCIdentifier(), contentHandler); 503 saxIfNotNull("source", dcObject.getDCSource(), contentHandler); 504 saxIfNotNull("language", dcObject.getDCLanguage(), contentHandler); 505 saxIfNotNull("relation", dcObject.getDCRelation(), contentHandler); 506 saxIfNotNull("coverage", dcObject.getDCCoverage(), contentHandler); 507 saxIfNotNull("rights", dcObject.getDCRights(), contentHandler); 508 XMLUtils.endElement(contentHandler, "dublin-core-metadata"); 509 } 510 511 /** 512 * Send a value if not null. 513 * @param name the tag name. 514 * @param value the value. 515 * @param contentHandler the ContentHandler receving SAX events. 516 * @throws SAXException if an error occurs during the SAX events generation. 517 */ 518 protected void saxIfNotNull(String name, String value, ContentHandler contentHandler) throws SAXException 519 { 520 if (value != null) 521 { 522 XMLUtils.createElement(contentHandler, name, value); 523 } 524 } 525 526 /** 527 * Send values if not null. 528 * @param name the tag name. 529 * @param values the values. 530 * @param contentHandler the ContentHandler receving SAX events. 531 * @throws SAXException if an error occurs during the SAX events generation. 532 */ 533 protected void saxIfNotNull(String name, String[] values, ContentHandler contentHandler) throws SAXException 534 { 535 if (values != null) 536 { 537 for (String value : values) 538 { 539 XMLUtils.createElement(contentHandler, name, value); 540 } 541 } 542 } 543 544 /** 545 * Send a value if not null. 546 * @param name the tag name. 547 * @param value the value. 548 * @param contentHandler the ContentHandler receving SAX events. 549 * @throws SAXException if an error occurs during the SAX events generation. 550 */ 551 protected void saxIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException 552 { 553 if (value != null) 554 { 555 LocalDate ld = DateUtils.asLocalDate(value); 556 XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE)); 557 } 558 } 559 560 /** 561 * Generates SAX events for content's comments. 562 * @param content the {@link Content}. 563 * @param contentHandler the ContentHandler receving SAX events. 564 * @throws SAXException if an error occurs during the SAX events generation. 565 */ 566 protected void saxContentComments(CommentableContent content, ContentHandler contentHandler) throws SAXException 567 { 568 List<Comment> comments = content.getComments(false, true); 569 int level = 0; 570 saxComments(comments, "comments", level, contentHandler); 571 } 572 573 /** 574 * Generates SAX events for comments. 575 * @param comments the comments. 576 * @param elementName the tag name. 577 * @param level the comments level. 578 * @param contentHandler the ContentHandler receving SAX events. 579 * @throws SAXException if an error occurs during the SAX events generation. 580 */ 581 protected void saxComments(List<Comment> comments, String elementName, int level, ContentHandler contentHandler) throws SAXException 582 { 583 if (comments.size() > 0) 584 { 585 XMLUtils.startElement(contentHandler, elementName); 586 587 for (Comment comment : comments) 588 { 589 saxComment(comment, level, contentHandler); 590 } 591 592 XMLUtils.endElement(contentHandler, elementName); 593 } 594 } 595 596 /** 597 * Generates SAX events for a single comment. 598 * @param comment the comment. 599 * @param level the comment level. 600 * @param contentHandler the ContentHandler receving SAX events. 601 * @throws SAXException if an error occurs during the SAX events generation. 602 */ 603 protected void saxComment(Comment comment, int level, ContentHandler contentHandler) throws SAXException 604 { 605 AttributesImpl attrs = new AttributesImpl(); 606 607 attrs.addCDATAAttribute("id", comment.getId()); 608 attrs.addCDATAAttribute("creation-date", DateUtils.zonedDateTimeToString(comment.getCreationDate())); 609 attrs.addCDATAAttribute("level", String.valueOf(level)); 610 attrs.addCDATAAttribute("is-validated", String.valueOf(comment.isValidated())); 611 attrs.addCDATAAttribute("is-email-hidden", String.valueOf(comment.isEmailHidden())); 612 613 if (!StringUtils.isBlank(comment.getAuthorName())) 614 { 615 attrs.addCDATAAttribute("author-name", comment.getAuthorName()); 616 } 617 618 if (!comment.isEmailHidden() && !StringUtils.isBlank(comment.getAuthorEmail())) 619 { 620 attrs.addCDATAAttribute("author-email", comment.getAuthorEmail()); 621 } 622 623 if (!StringUtils.isBlank(comment.getAuthorURL())) 624 { 625 attrs.addCDATAAttribute("author-url", comment.getAuthorURL()); 626 } 627 628 XMLUtils.startElement(contentHandler, "comment", attrs); 629 630 if (comment.getContent() != null) 631 { 632 String[] contents = comment.getContent().split("\r?\n"); 633 for (String c : contents) 634 { 635 XMLUtils.createElement(contentHandler, "p", c); 636 } 637 } 638 639 // The generated SAXed events for the comments' reaction have changed. 640 // In the SAXed events of the ContentGenerator (that should one day use this ContentSaxer): 641 // - there is a "nb-like" attributes on the comment node that disappears here 642 // - reactions are SAXed as a simple list of ("likers") 643 saxReactions(comment, contentHandler); 644 645 saxReports(comment, contentHandler); 646 647 saxComments(comment.getSubComment(false, true), "sub-comments", level + 1, contentHandler); 648 649 XMLUtils.endElement(contentHandler, "comment"); 650 } 651 652 /** 653 * Generates SAX events for the given object's reactions. 654 * @param reactionable the {@link ReactionableObject}. 655 * @param contentHandler the ContentHandler receiving SAX events. 656 * @throws SAXException if an error occurs during the SAX events generation. 657 */ 658 protected void saxReactions(ReactionableObject reactionable, ContentHandler contentHandler) throws SAXException 659 { 660 XMLUtils.startElement(contentHandler, "reactions"); 661 662 for (ReactionType reactionType : ReactionType.values()) 663 { 664 List<UserIdentity> actors = reactionable.getReactionUsers(reactionType); 665 if (!actors.isEmpty()) 666 { 667 AttributesImpl attrs = new AttributesImpl(); 668 attrs.addCDATAAttribute("type", reactionType.name()); 669 XMLUtils.startElement(contentHandler, "reaction", attrs); 670 671 for (UserIdentity actor : actors) 672 { 673 _userHelper.saxUserIdentity(actor, contentHandler, "actor"); 674 } 675 676 XMLUtils.endElement(contentHandler, "reaction"); 677 } 678 } 679 XMLUtils.endElement(contentHandler, "reactions"); 680 } 681 682 /** 683 * Generates SAX events for the given object's reports. 684 * @param reportable the {@link Content}. 685 * @param contentHandler the ContentHandler receiving SAX events. 686 * @throws SAXException if an error occurs during the SAX events generation. 687 */ 688 protected void saxReports(ReportableObject reportable, ContentHandler contentHandler) throws SAXException 689 { 690 long reportsCount = reportable.getReportsCount(); 691 if (reportsCount > 0) 692 { 693 AttributesImpl attrs = new AttributesImpl(); 694 attrs.addCDATAAttribute("count", String.valueOf(reportsCount)); 695 XMLUtils.createElement(contentHandler, "reports", attrs); 696 } 697 } 698 699 /** 700 * Fills the given content with the values from the provided {@link org.w3c.dom.Node}. 701 * <br>This is the anti-operation of {@link #saxContent}, as the org.w3c.dom.Node should be a Node previously generated with SAX events from this method. 702 * @param content The content to fill 703 * @param node The node to read for retrieving values to fill 704 * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes 705 * @throws Exception if an exception occurs 706 */ 707 public void fillContent(ModifiableContent content, org.w3c.dom.Node node, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception 708 { 709 org.w3c.dom.Node contentNode = XPathAPI.selectSingleNode(node, "content"); 710 711 fillAttributes(content, contentNode, additionalDataGetter); 712 fillDublinCore(content, contentNode); 713 714 if (content instanceof CommentableContent) 715 { 716 fillContentComments((CommentableContent) content, contentNode); 717 } 718 719 if (content instanceof ReactionableObject) 720 { 721 fillReactions((ReactionableObject) content, contentNode); 722 } 723 724 if (content instanceof ReportableObject) 725 { 726 fillReports((ReportableObject) content, contentNode); 727 } 728 } 729 730 /** 731 * Fills the given object with the dublin core values from the provided {@link org.w3c.dom.Node}. 732 * @param dcObject The object to fill 733 * @param node The node to read to get the values to fill 734 * @throws Exception if an exception occurs 735 */ 736 protected void fillDublinCore(ModifiableDublinCoreAwareAmetysObject dcObject, org.w3c.dom.Node node) throws Exception 737 { 738 org.w3c.dom.Node dcNode = XPathAPI.selectSingleNode(node, "dublin-core-metadata"); 739 if (dcNode != null) 740 { 741 setIfNotNull(dcNode, "title", XObject::str, dcObject::setDCTitle); 742 setIfNotNull(dcNode, "creator", XObject::str, dcObject::setDCCreator); 743 setIfNotNull(dcNode, "subject", this::values, dcObject::setDCSubject); 744 setIfNotNull(dcNode, "description", XObject::str, dcObject::setDCDescription); 745 setIfNotNull(dcNode, "publisher", XObject::str, dcObject::setDCPublisher); 746 setIfNotNull(dcNode, "contributor", XObject::str, dcObject::setDCContributor); 747 setIfNotNull(dcNode, "date", this::dateValue, dcObject::setDCDate); 748 setIfNotNull(dcNode, "type", XObject::str, dcObject::setDCType); 749 setIfNotNull(dcNode, "format", XObject::str, dcObject::setDCFormat); 750 setIfNotNull(dcNode, "identifier", XObject::str, dcObject::setDCIdentifier); 751 setIfNotNull(dcNode, "source", XObject::str, dcObject::setDCSource); 752 setIfNotNull(dcNode, "language", XObject::str, dcObject::setDCLanguage); 753 setIfNotNull(dcNode, "relation", XObject::str, dcObject::setDCRelation); 754 setIfNotNull(dcNode, "coverage", XObject::str, dcObject::setDCCoverage); 755 setIfNotNull(dcNode, "rights", XObject::str, dcObject::setDCRights); 756 } 757 } 758 759 /** 760 * Fills the given content with the comments from the provided {@link org.w3c.dom.Node} 761 * @param content The content to fill 762 * @param contentNode the node to read to get the comments' values 763 * @throws Exception if an error occurs 764 */ 765 protected void fillContentComments(CommentableContent content, org.w3c.dom.Node contentNode) throws Exception 766 { 767 NodeList commnentsNodes = XPathAPI.selectNodeList(contentNode, "comments/comment"); 768 for (int i = 0; i < commnentsNodes.getLength(); i++) 769 { 770 org.w3c.dom.Node commentNode = commnentsNodes.item(i); 771 772 String commentId = XPathAPI.eval(commentNode, "@id").str(); 773 String creationDateAsString = XPathAPI.eval(commentNode, "@creation-date").str(); 774 ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString); 775 776 Comment comment = content.createComment(commentId, creationDate); 777 fillComment(comment, commentNode); 778 } 779 } 780 781 /** 782 * Fills the given comment with the values from the provided {@link org.w3c.dom.Node} 783 * @param comment The comment to fill 784 * @param commentNode the node to read to get the comment's values 785 * @throws Exception if an error occurs 786 */ 787 protected void fillComment(Comment comment, org.w3c.dom.Node commentNode) throws Exception 788 { 789 setIfNotNull(commentNode, "@is-validated", this::booleanValue, comment::setValidated); 790 setIfNotNull(commentNode, "@is-email-hidden", this::booleanValue, comment::setEmailHiddenStatus); 791 setIfNotNull(commentNode, "@author-name", XObject::str, comment::setAuthorName); 792 setIfNotNull(commentNode, "@author-email", XObject::str, comment::setAuthorEmail); 793 setIfNotNull(commentNode, "@author-url", XObject::str, comment::setAuthorURL); 794 795 StringBuilder content = new StringBuilder(); 796 NodeList paragraphs = XPathAPI.selectNodeList(commentNode, "p"); 797 for (int i = 0; i < paragraphs.getLength(); i++) 798 { 799 org.w3c.dom.Node paragraph = paragraphs.item(i); 800 content.append(paragraph.getTextContent()) 801 .append("\r\n"); 802 } 803 comment.setContent(content.toString()); 804 805 fillReactions(comment, commentNode); 806 807 fillReports(comment, commentNode); 808 809 NodeList subCommnentsNodes = XPathAPI.selectNodeList(commentNode, "sub-comments/comment"); 810 for (int i = 0; i < subCommnentsNodes.getLength(); i++) 811 { 812 org.w3c.dom.Node subCommentNode = subCommnentsNodes.item(i); 813 814 String subCommentId = XPathAPI.eval(subCommentNode, "@id").str(); 815 String creationDateAsString = XPathAPI.eval(subCommentNode, "@creation-date").str(); 816 ZonedDateTime creationDate = DateUtils.parseZonedDateTime(creationDateAsString); 817 818 Comment subComment = comment.createSubComment(subCommentId, creationDate); 819 fillComment(subComment, subCommentNode); 820 } 821 } 822 823 /** 824 * Fills the given {@link ReactionableObject} with the reactions from the provided {@link org.w3c.dom.Node} 825 * @param reactionable The {@link ReactionableObject} to fill 826 * @param node the node to read to get the reactions 827 * @throws Exception if an error occurs 828 */ 829 protected void fillReactions(ReactionableObject reactionable, org.w3c.dom.Node node) throws Exception 830 { 831 NodeList reactions = XPathAPI.selectNodeList(node, "reactions/reaction"); 832 for (int i = 0; i < reactions.getLength(); i++) 833 { 834 org.w3c.dom.Node reactionNode = reactions.item(i); 835 XObject reactionTypeAttr = XPathAPI.eval(reactionNode, "@type"); 836 ReactionType reactionType = ReactionType.valueOf(reactionTypeAttr.str()); 837 NodeList actors = XPathAPI.selectNodeList(reactionNode, "actor"); 838 for (int j = 0; j < actors.getLength(); j++) 839 { 840 org.w3c.dom.Node actorNode = actors.item(j); 841 UserIdentity actor = _userHelper.xml2userIdentity(actorNode); 842 reactionable.addReaction(actor, reactionType); 843 } 844 } 845 } 846 847 /** 848 * Fills the given {@link ReportableObject} with the reports from the provided {@link org.w3c.dom.Node} 849 * @param reportable The {@link ReportableObject} to fill 850 * @param node the node to read to get the reports 851 * @throws Exception if an error occurs 852 */ 853 protected void fillReports(ReportableObject reportable, org.w3c.dom.Node node) throws Exception 854 { 855 org.w3c.dom.Node reportsNode = XPathAPI.selectSingleNode(node, "reports"); 856 if (reportsNode != null) 857 { 858 XObject reportsCountAttr = XPathAPI.eval(reportsNode, "@count"); 859 long reportsCount = (long) reportsCountAttr.num(); 860 861 if (reportsCount > 0) 862 { 863 reportable.setReportsCount(reportsCount); 864 } 865 } 866 } 867 868 /** 869 * Fills the given content with the attributes from the provided {@link org.w3c.dom.Node} 870 * @param content The content to fill 871 * @param contentNode the node to read to get the attributes 872 * @param additionalDataGetter The object that will retrieve potential additional data for the content's attributes 873 * @throws Exception if an error occurs 874 */ 875 protected void fillAttributes(ModifiableContent content, org.w3c.dom.Node contentNode, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws Exception 876 { 877 Element attributesElement = (Element) XPathAPI.selectSingleNode(contentNode, "attributes"); 878 @SuppressWarnings("unchecked") 879 Collection<Model> contentModels = (Collection<Model>) content.getModel(); 880 View view = View.of(contentModels); 881 Map<String, Object> values = new ModelAwareXMLValuesExtractor(attributesElement, additionalDataGetter, contentModels) 882 .extractValues(view); 883 884 content.synchronizeValues(view, values); 885 } 886 887 /** 888 * Sets a value through the given setter if the value is not null 889 * @param <T> The type of the value to set 890 * @param node The node to read to get the value 891 * @param expression The expression to apply on the node to get the value 892 * @param retriever The {@link Function} that will retrieve the typed value from the {@link XObject} evaluated from the expression 893 * @param setter The {@link Consumer} that will be used to set the value 894 * @throws Exception if an error occurs 895 */ 896 protected <T> void setIfNotNull(org.w3c.dom.Node node, String expression, ThrowingFunction<XObject, T> retriever, Consumer<T> setter) throws Exception 897 { 898 XObject xObject = XPathAPI.eval(node, expression); 899 if (xObject.getType() != XObject.CLASS_NULL) 900 { 901 T value = retriever.apply(xObject); 902 if (value != null) 903 { 904 setter.accept(value); 905 } 906 } 907 } 908 909 /** 910 * Consumes an {@link XObject} to retrieve the value as a {@link Date} 911 * @param xObject The consumed {@link XObject} 912 * @return The {@link Date} value 913 */ 914 protected Date dateValue(XObject xObject) 915 { 916 String dateAsString = xObject.str(); 917 if (StringUtils.isBlank(dateAsString)) 918 { 919 return null; 920 } 921 LocalDate localDate = DateTimeFormatter.ISO_LOCAL_DATE.parse(dateAsString, LocalDate::from); 922 return DateUtils.asDate(localDate); 923 } 924 925 /** 926 * Consumes an {@link XObject} to retrieve the value as a {@link Boolean} 927 * @param xObject The consumed {@link XObject} 928 * @return The {@link Boolean} value 929 */ 930 protected Boolean booleanValue(XObject xObject) 931 { 932 String booleanAsString = xObject.str(); 933 return Boolean.valueOf(booleanAsString); 934 } 935 936 /** 937 * Consumes an {@link XObject} to retrieve the value as a String array 938 * @param xObject The consumed {@link XObject} 939 * @return The String array 940 * @throws TransformerException if an error occurs 941 */ 942 protected String[] values(XObject xObject) throws TransformerException 943 { 944 NodeList nodeList = xObject.nodelist(); 945 return IntStream.range(0, nodeList.getLength()) 946 .mapToObj(nodeList::item) 947 .map(org.w3c.dom.Node::getTextContent) 948 .toArray(String[]::new); 949 } 950}