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