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}