001/* 002 * Copyright 2010 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.plugins.newsletter.workflow; 017 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.InputStreamReader; 021import java.io.Reader; 022import java.io.StringReader; 023import java.io.UnsupportedEncodingException; 024import java.text.DateFormat; 025import java.text.SimpleDateFormat; 026import java.util.Date; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030import java.util.Properties; 031import java.util.Set; 032 033import javax.xml.parsers.ParserConfigurationException; 034import javax.xml.parsers.SAXParserFactory; 035import javax.xml.transform.OutputKeys; 036import javax.xml.transform.Transformer; 037import javax.xml.transform.TransformerException; 038import javax.xml.transform.TransformerFactory; 039import javax.xml.transform.sax.SAXSource; 040import javax.xml.transform.stream.StreamResult; 041 042import org.apache.avalon.framework.activity.Initializable; 043import org.apache.avalon.framework.context.Context; 044import org.apache.avalon.framework.context.ContextException; 045import org.apache.avalon.framework.context.Contextualizable; 046import org.apache.avalon.framework.service.ServiceException; 047import org.apache.avalon.framework.service.ServiceManager; 048import org.apache.cocoon.components.ContextHelper; 049import org.apache.cocoon.components.source.impl.SitemapSource; 050import org.apache.cocoon.environment.Request; 051import org.apache.cocoon.xml.AttributesImpl; 052import org.apache.cocoon.xml.SaxBuffer; 053import org.apache.commons.io.IOUtils; 054import org.apache.commons.lang.StringUtils; 055import org.apache.excalibur.source.SourceResolver; 056import org.apache.excalibur.xml.sax.ContentHandlerProxy; 057import org.xml.sax.Attributes; 058import org.xml.sax.ContentHandler; 059import org.xml.sax.InputSource; 060import org.xml.sax.SAXException; 061import org.xml.sax.XMLReader; 062import org.xml.sax.helpers.XMLFilterImpl; 063 064import org.ametys.cms.data.RichText; 065import org.ametys.cms.repository.ModifiableContent; 066import org.ametys.core.util.DateUtils; 067import org.ametys.plugins.newsletter.auto.AutomaticNewsletterFilterResult; 068import org.ametys.plugins.newsletter.category.Category; 069import org.ametys.plugins.newsletter.category.CategoryProvider; 070import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint; 071import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 072import org.ametys.plugins.repository.model.RepositoryDataContext; 073import org.ametys.runtime.i18n.I18nizableText; 074import org.ametys.runtime.model.ElementDefinition; 075import org.ametys.runtime.model.type.DataContext; 076import org.ametys.runtime.model.type.ElementType; 077import org.ametys.web.repository.site.Site; 078import org.ametys.web.workflow.CreateContentFunction; 079 080import com.opensymphony.workflow.WorkflowException; 081 082/** 083 * OSWorkflow function for creating a content. 084 */ 085public class CreateNewsletterFunction extends CreateContentFunction implements Initializable, Contextualizable 086{ 087 088 /** Newsletter category key. */ 089 public static final String NEWSLETTER_CATEGORY_KEY = CreateNewsletterFunction.class.getName() + "$category"; 090 /** Newsletter number key. */ 091 public static final String NEWSLETTER_NUMBER_KEY = CreateNewsletterFunction.class.getName() + "$number"; 092 /** Newsletter date key. */ 093 public static final String NEWSLETTER_DATE_KEY = CreateNewsletterFunction.class.getName() + "$date"; 094 /** Newsletter automatic property key. */ 095 public static final String NEWSLETTER_IS_AUTOMATIC_KEY = CreateNewsletterFunction.class.getName() + "$isAutomatic"; 096 /** Key for "process auto sections". */ 097 public static final String NEWSLETTER_PROCESS_AUTO_SECTIONS_KEY = CreateNewsletterFunction.class.getName() + "$processAutoSections"; 098 /** Key for content ID map when processing auto sections. */ 099 public static final String NEWSLETTER_CONTENT_ID_MAP_KEY = CreateNewsletterFunction.class.getName() + "$contentIds"; 100 101 /** The date format. */ 102 public static final DateFormat NEWSLETTER_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); 103 104 /** The list of attributes to ignore. */ 105 protected static final Set<String> _IGNORE_ATTRS = new HashSet<>(); 106 static 107 { 108 _IGNORE_ATTRS.add("auto-newsletter-ignore"); 109 _IGNORE_ATTRS.add("auto-newsletter-ignore-if-empty"); 110 _IGNORE_ATTRS.add("auto-newsletter-insert-filter"); 111 _IGNORE_ATTRS.add("auto-newsletter-insert-level"); 112 } 113 114 /** The defaut content insertion level. */ 115 protected static final String _DEFAULT_LEVEL = "3"; 116 117 /** 118 * The Avalon context 119 */ 120 protected Context _context; 121 122 private SourceResolver _sourceResolver; 123 private CategoryProviderExtensionPoint _categoryProviderEP; 124 125 // Transformation objects. 126 private TransformerFactory _transformerFactory; 127 private Properties _transformerProperties; 128 private SAXParserFactory _saxParserFactory; 129 130 @Override 131 public void contextualize(Context context) throws ContextException 132 { 133 _context = context; 134 } 135 136 @Override 137 public void service(ServiceManager manager) throws ServiceException 138 { 139 super.service(manager); 140 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 141 _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE); 142 } 143 144 @Override 145 public void initialize() throws Exception 146 { 147 // Initialize transformation objects. 148 _transformerFactory = TransformerFactory.newInstance(); 149 150 _transformerProperties = new Properties(); 151 _transformerProperties.put(OutputKeys.METHOD, "xml"); 152 _transformerProperties.put(OutputKeys.ENCODING, "UTF-8"); 153 _transformerProperties.put(OutputKeys.OMIT_XML_DECLARATION, "yes"); 154 155 _saxParserFactory = SAXParserFactory.newInstance(); 156 _saxParserFactory.setNamespaceAware(true); 157 } 158 159 @Override 160 public I18nizableText getLabel() 161 { 162 return new I18nizableText("plugin.newsletter", "PLUGINS_NEWSLETTER_CREATE_NEWSLETTER_FUNCTION_LABEL"); 163 } 164 165 @Override 166 protected void _populateContent(Map transientVars, ModifiableContent content) throws WorkflowException 167 { 168 super._populateContent(transientVars, content); 169 170 String category = (String) transientVars.get(NEWSLETTER_CATEGORY_KEY); 171 if (category == null) 172 { 173 throw new WorkflowException("Missing category"); 174 } 175 176 Long number = (Long) transientVars.get(NEWSLETTER_NUMBER_KEY); 177 Date date = (Date) transientVars.get(NEWSLETTER_DATE_KEY); 178 boolean isAutomatic = "true".equals(transientVars.get(NEWSLETTER_IS_AUTOMATIC_KEY)); 179 boolean processAutoSections = "true".equals(transientVars.get(NEWSLETTER_PROCESS_AUTO_SECTIONS_KEY)); 180 @SuppressWarnings("unchecked") 181 Map<String, AutomaticNewsletterFilterResult> filterResults = (Map<String, AutomaticNewsletterFilterResult>) transientVars.get(NEWSLETTER_CONTENT_ID_MAP_KEY); 182 183 if (processAutoSections && filterResults == null) 184 { 185 throw new WorkflowException("Content ID map must not be null if processing automatic sections."); 186 } 187 188 189 ModifiableModelLessDataHolder internalDataHolder = content.getInternalDataHolder(); 190 internalDataHolder.setValue("category", category); 191 internalDataHolder.setValue("automatic", isAutomatic); 192 193 if (number != null) 194 { 195 content.setValue("newsletter-number", number); 196 } 197 if (date != null) 198 { 199 content.setValue("newsletter-date", DateUtils.asLocalDate(date)); 200 } 201 202 String siteName = (String) transientVars.get(SITE_KEY); 203 Site site = _siteManager.getSite(siteName); 204 205 // Initialize the content from the model 206 _initContentRichText (content, site.getSkinId(), category, processAutoSections, filterResults); 207 } 208 209 private void _initContentRichText (ModifiableContent content, String skinId, String categoryID, boolean processAutoSections, Map<String, AutomaticNewsletterFilterResult> filterResults) throws WorkflowException 210 { 211 Set<String> ids = _categoryProviderEP.getExtensionsIds(); 212 for (String id : ids) 213 { 214 CategoryProvider provider = _categoryProviderEP.getExtension(id); 215 if (provider.hasCategory(categoryID)) 216 { 217 Category category = provider.getCategory(categoryID); 218 String templateId = category.getTemplate(); 219 220 if (templateId == null) 221 { 222 throw new WorkflowException ("The template can not be null"); 223 } 224 225 try 226 { 227 String text = _getContent (skinId, templateId); 228 229 // Process automatic newsletter tags. 230 String processedText = _processAutoTags(text, processAutoSections, filterResults); 231 232 ElementType<RichText> richTextType = ((ElementDefinition) content.getDefinition("content")).getType(); 233 234 DataContext dataContext = RepositoryDataContext.newInstance() 235 .withObject(content) 236 .withDataPath("content"); 237 238 content.setValue("content", richTextType.fromJSONForClient(processedText, dataContext)); 239 content.saveChanges(); 240 } 241 catch (IOException e) 242 { 243 throw new WorkflowException("Unable to transform rich text", e); 244 } 245 } 246 } 247 } 248 249 private String _processAutoTags(String text, boolean processAutoSections, Map<String, AutomaticNewsletterFilterResult> filterResults) throws WorkflowException 250 { 251 StringReader in = new StringReader(text); 252 ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); 253 254 try 255 { 256 Transformer transformer = _transformerFactory.newTransformer(); 257 transformer.setOutputProperties(_transformerProperties); 258 XMLReader xmlReader = _saxParserFactory.newSAXParser().getXMLReader(); 259 260 NewsletterFilter newsletterFilter = new NewsletterFilter(xmlReader, _sourceResolver, processAutoSections, filterResults); 261 SAXSource transformSource = new SAXSource(newsletterFilter, new InputSource(in)); 262 263 transformer.transform(transformSource, new StreamResult(baos)); 264 265 return baos.toString("UTF-8"); 266 } 267 catch (TransformerException e) 268 { 269 throw new WorkflowException("Transformer exception.", e); 270 } 271 catch (SAXException e) 272 { 273 throw new WorkflowException("SAX exception.", e); 274 } 275 catch (ParserConfigurationException e) 276 { 277 throw new WorkflowException("SAX exception.", e); 278 } 279 catch (UnsupportedEncodingException e) 280 { 281 throw new WorkflowException("Unsupported encoding.", e); 282 } 283 } 284 285 private String _getContent (String skinId, String templateId) throws IOException, WorkflowException 286 { 287 SitemapSource src = null; 288 Request request = ContextHelper.getRequest(_context); 289 if (request == null) 290 { 291 throw new WorkflowException("Unable to get the request"); 292 } 293 294 try 295 { 296 request.setAttribute("skin", skinId); 297 src = (SitemapSource) _sourceResolver.resolveURI("cocoon://_plugins/newsletter/" + templateId + "/model.xml"); 298 Reader reader = new InputStreamReader(src.getInputStream(), "UTF-8"); 299 return IOUtils.toString(reader); 300 } 301 finally 302 { 303 _sourceResolver.release(src); 304 } 305 } 306 307 /** 308 * Automatic newsletter filter. 309 */ 310 protected class NewsletterFilter extends XMLFilterImpl 311 { 312 313 private SourceResolver _srcResolver; 314 315 private boolean _processAutoSections; 316 317 private Map<String, AutomaticNewsletterFilterResult> _filterResults; 318 319 private boolean _ignore; 320 321 private boolean _ignoreNextLevel; 322 323 private int _ignoreDepth; 324 325 /** 326 * Constructor. 327 * @param xmlReader the parent XML reader. 328 * @param sourceResolver the source resolver. 329 * @param filterResults the filter results. 330 * @param processAutoSections true to process auto sections, false to ignore them. 331 */ 332 public NewsletterFilter(XMLReader xmlReader, SourceResolver sourceResolver, boolean processAutoSections, Map<String, AutomaticNewsletterFilterResult> filterResults) 333 { 334 super(xmlReader); 335 _srcResolver = sourceResolver; 336 _processAutoSections = processAutoSections; 337 _filterResults = filterResults; 338 } 339 340 @Override 341 public void startDocument() throws SAXException 342 { 343 super.startDocument(); 344 _ignore = false; 345 _ignoreDepth = 0; 346 } 347 348 @Override 349 public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException 350 { 351 String ignore = atts.getValue("auto-newsletter-ignore"); 352 String ignoreIfEmpty = atts.getValue("auto-newsletter-ignore-if-empty"); 353 String insertFilter = atts.getValue("auto-newsletter-insert-filter"); 354 String insertFilterLevel = atts.getValue("auto-newsletter-insert-level"); 355 356 if (_ignoreNextLevel) 357 { 358 _ignoreNextLevel = false; 359 _ignore = true; 360 _ignoreDepth = 0; 361 } 362 363 if (StringUtils.isNotEmpty(ignore) || StringUtils.isNotEmpty(ignoreIfEmpty) || StringUtils.isNotEmpty(insertFilter) || StringUtils.isNotEmpty(insertFilterLevel)) 364 { 365 AttributesImpl newAtts = new AttributesImpl(); 366 367 // Copy all attributes except auto newsletter ones. 368 _copyStartElementAttributes(atts, newAtts); 369 370 SaxBuffer saxBuffer = null; 371 372 if (_processAutoSections && !_ignore) 373 { 374 if ("true".equals(ignore)) 375 { 376 _ignore = true; 377 _ignoreDepth = 0; 378 } 379 else if (StringUtils.isNotEmpty(ignoreIfEmpty)) 380 { 381 _handleIgnoreIfEmpty(ignoreIfEmpty); 382 } 383 else if (StringUtils.isNotEmpty(insertFilter)) 384 { 385 saxBuffer = _handleInsertFilter(insertFilter, insertFilterLevel, saxBuffer); 386 } 387 } 388 389 if (!_ignore) 390 { 391 super.startElement(uri, localName, qName, newAtts); 392 393 if (saxBuffer != null) 394 { 395 _ignoreNextLevel = true; 396 397 saxBuffer.toSAX(getContentHandler()); 398 } 399 } 400 else 401 { 402 _ignoreDepth++; 403 } 404 } 405 else 406 { 407 // No attribute found, no need to transform anything. 408 if (!_ignore) 409 { 410 super.startElement(uri, localName, qName, atts); 411 } 412 else 413 { 414 _ignoreDepth++; 415 } 416 } 417 } 418 419 private void _handleIgnoreIfEmpty(String ignoreIfEmpty) 420 { 421 if (!_filterResults.containsKey(ignoreIfEmpty) || !_filterResults.get(ignoreIfEmpty).hasResults()) 422 { 423 _ignore = true; 424 _ignoreDepth = 0; 425 } 426 } 427 428 private SaxBuffer _handleInsertFilter(String insertFilter, String insertFilterLevel, SaxBuffer saxBuffer) throws SAXException 429 { 430 SaxBuffer modifiedSaxBuffer = saxBuffer; 431 432 if (_filterResults.containsKey(insertFilter) && _filterResults.get(insertFilter).hasResults()) 433 { 434 AutomaticNewsletterFilterResult result = _filterResults.get(insertFilter); 435 List<String> contentIds = result.getContentIds(); 436 437 String viewName = result.getViewName(); 438 String level = StringUtils.defaultIfEmpty(insertFilterLevel, _DEFAULT_LEVEL); 439 440 modifiedSaxBuffer = _getFilterContent(contentIds, level, viewName); 441 } 442 else 443 { 444 _ignore = true; 445 _ignoreDepth = 0; 446 } 447 448 return modifiedSaxBuffer; 449 } 450 451 private void _copyStartElementAttributes(Attributes atts, AttributesImpl newAtts) 452 { 453 for (int i = 0; i < atts.getLength(); i++) 454 { 455 String attrName = atts.getLocalName(i); 456 if (!_IGNORE_ATTRS.contains(attrName)) 457 { 458 newAtts.addAttribute(atts.getURI(i), attrName, atts.getQName(i), atts.getType(i), atts.getValue(i)); 459 } 460 } 461 } 462 463 @Override 464 public void endElement(String uri, String localName, String qName) throws SAXException 465 { 466 if (_ignoreNextLevel) 467 { 468 _ignoreNextLevel = false; 469 } 470 471 if (!_ignore) 472 { 473 super.endElement(uri, localName, qName); 474 } 475 else 476 { 477 _ignoreDepth--; 478 479 if (_ignoreDepth < 1) 480 { 481 _ignore = false; 482 } 483 } 484 } 485 486 @Override 487 public void characters(char[] ch, int start, int length) throws SAXException 488 { 489 if (!_ignore && !_ignoreNextLevel) 490 { 491 super.characters(ch, start, length); 492 } 493 } 494 495 private SaxBuffer _getFilterContent(List<String> contentIds, String level, String viewName) throws SAXException 496 { 497 SitemapSource src = null; 498 Request request = ContextHelper.getRequest(_context); 499 if (request == null) 500 { 501 throw new SAXException("Unable to get the request"); 502 } 503 504 try 505 { 506 StringBuilder url = new StringBuilder("cocoon://_plugins/web/contents/last-published"); 507 url.append("?viewName=").append(viewName).append("&level=").append(level); 508 for (String id : contentIds) 509 { 510 url.append("&contentId=").append(id); 511 } 512 513 src = (SitemapSource) _srcResolver.resolveURI(url.toString()); 514 515 SaxBuffer buffer = new SaxBuffer(); 516 517 // Ignore the root tag 518 src.toSAX(new IgnoreRootTagHandler(buffer)); 519 520 return buffer; 521 } 522 catch (IOException e) 523 { 524 throw new SAXException("Error resolving the contents.", e); 525 } 526 finally 527 { 528 _srcResolver.release(src); 529 } 530 } 531 532 } 533 534 /** 535 * Ignore the root tag. 536 */ 537 protected class IgnoreRootTagHandler extends ContentHandlerProxy 538 { 539 private int _depth; 540 541 /** 542 * Constructor 543 * @param contentHandler the contentHandler to pass SAX events to. 544 */ 545 public IgnoreRootTagHandler(ContentHandler contentHandler) 546 { 547 super(contentHandler); 548 } 549 550 @Override 551 public void startDocument() throws SAXException 552 { 553 _depth = 0; 554 } 555 556 @Override 557 public void endDocument() throws SAXException 558 { 559 // empty method 560 } 561 562 @Override 563 public void startElement(String uri, String loc, String raw, Attributes a) throws SAXException 564 { 565 _depth++; 566 567 if (_depth > 1) 568 { 569 super.startElement(uri, loc, raw, a); 570 } 571 } 572 573 @Override 574 public void endElement(String uri, String loc, String raw) throws SAXException 575 { 576 if (_depth > 1) 577 { 578 super.endElement(uri, loc, raw); 579 } 580 581 _depth--; 582 } 583 } 584}