001/*
002 *  Copyright 2015 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.syndication;
017
018import java.io.IOException;
019import java.text.SimpleDateFormat;
020import java.util.Date;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.ProcessingException;
028import org.apache.cocoon.environment.ObjectModelHelper;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.generation.ServiceableGenerator;
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.lang.StringUtils;
034import org.xml.sax.SAXException;
035
036import org.ametys.runtime.config.Config;
037import org.ametys.web.renderingcontext.RenderingContext;
038import org.ametys.web.renderingcontext.RenderingContextHandler;
039import org.ametys.web.repository.page.Page;
040import org.ametys.web.repository.site.Site;
041
042import com.rometools.modules.mediarss.MediaEntryModule;
043import com.rometools.modules.mediarss.MediaModule;
044import com.rometools.modules.mediarss.types.MediaContent;
045import com.rometools.modules.mediarss.types.MediaGroup;
046import com.rometools.modules.mediarss.types.Metadata;
047import com.rometools.modules.mediarss.types.Reference;
048import com.rometools.modules.mediarss.types.Thumbnail;
049import com.rometools.modules.mediarss.types.UrlReference;
050import com.rometools.rome.feed.synd.SyndCategory;
051import com.rometools.rome.feed.synd.SyndContent;
052import com.rometools.rome.feed.synd.SyndEnclosure;
053import com.rometools.rome.feed.synd.SyndEntry;
054import com.rometools.rome.feed.synd.SyndFeed;
055import com.rometools.rome.feed.synd.SyndImage;
056import com.rometools.rome.feed.synd.SyndPerson;
057
058/**
059 * Generates the contents of an external RSS of Atom feed.<br>
060 * This implementation is based on the ROME API.
061 */
062public class FeedGenerator extends ServiceableGenerator
063{
064    /** The feed cache */
065    protected FeedCache _feedCache;
066    
067    /** The rendering context handler. */
068    protected RenderingContextHandler _renderingContext;
069    
070    @Override
071    public void service(ServiceManager serviceManager) throws ServiceException
072    {
073        super.service(serviceManager);
074        _feedCache = (FeedCache) serviceManager.lookup(FeedCache.ROLE);
075        _renderingContext = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
076    }
077    
078    @Override
079    public void generate() throws IOException, SAXException, ProcessingException
080    {
081        boolean isCustom = false;
082        boolean isSelected = false;
083        long length = -1;
084        String url;
085        String name = "";
086        int lifeTime = 0;
087        
088        @SuppressWarnings("unchecked")
089        Map<String, Object> ctxParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
090        if (ctxParameters != null)
091        {
092            url = (String) ctxParameters.get("url");
093            name = (String) ctxParameters.get("name");
094            lifeTime = (Integer) ctxParameters.get("cache");
095            length = (Long) ctxParameters.get("length");
096            isCustom = (Boolean) ctxParameters.get("isCustom");
097            isSelected = (Boolean) ctxParameters.get("isSelected");
098        }
099        else
100        {
101            url = source;
102        }
103        
104        contentHandler.startDocument();
105
106        if (_checkForInfiniteLoop(url))
107        {
108            getLogger().error("Infinite loop detected for RSS feed : " + url + ". The RSS feed url can not be the page itself.");
109            if (isCustom && _renderingContext.getRenderingContext() == RenderingContext.FRONT)
110            {
111                _saxErrorFeed("feed-error-custom", url, name, null);
112            }
113            else if (_renderingContext.getRenderingContext() == RenderingContext.BACK)
114            {
115                _saxErrorFeed("feed-error", url, name, "Infinite loop detected for RSS feed : " + url + ". The RSS feed url can not be the page itself.");
116            }
117        }
118        else
119        {
120            FeedResult feedResult = _feedCache.getFeed(url, lifeTime);
121            
122            if (feedResult.getStatus() == FeedResult.STATUS_OK)
123            {
124                SyndFeed feed = feedResult.getSynFeed();
125                _saxFeeds(feed, length, url, isCustom, isSelected);
126            }
127            else if (_renderingContext.getRenderingContext() == RenderingContext.BACK)
128            {
129                _saxErrorFeed("feed-error", url, name, feedResult.getMessageError());
130            }
131            else if (isCustom && _renderingContext.getRenderingContext() == RenderingContext.FRONT)
132            {
133                _saxErrorFeed("feed-error-custom", url, name, null);
134            }
135        }
136
137        contentHandler.endDocument();
138    }
139    
140    private void _saxErrorFeed (String errorName, String url, String name, String messageError) throws SAXException
141    {
142        AttributesImpl atts = new AttributesImpl();
143        _addAttribute(atts, "url", url);
144        _addAttribute(atts, "name", name);
145        if (StringUtils.isNotEmpty(messageError))
146        {
147            _addAttribute(atts, "messageError", messageError);
148        }
149
150        XMLUtils.createElement(contentHandler, errorName, atts);
151    }
152    
153    private void _saxFeeds (SyndFeed feed, long length, String url, Boolean isCustom, Boolean isSelected) throws SAXException
154    {
155        AttributesImpl atts = new AttributesImpl();
156
157        _addAttribute(atts, "title", feed.getTitle());
158        _addAttribute(atts, "description", feed.getDescription());
159        _addAttribute(atts, "feedUrl", url);
160        _addAttribute(atts, "isCustom", String.valueOf(isCustom));
161        _addAttribute(atts, "isSelected", String.valueOf(isSelected));
162
163        XMLUtils.startElement(contentHandler, "feed", atts);
164        
165        SyndImage image = feed.getImage();
166        
167        if (image != null)
168        {
169            atts = new AttributesImpl();
170            _addAttribute(atts, "link", image.getLink());
171            _addAttribute(atts, "url", image.getUrl());
172            _addAttribute(atts, "title", image.getTitle());
173            
174            XMLUtils.createElement(contentHandler, "image", atts);
175        }
176        
177        List<SyndCategory> feedCategories = feed.getCategories();
178        _saxCategories(feedCategories);
179        
180        List<SyndEntry> entries = feed.getEntries();
181        if (entries != null)
182        {
183            int index = 0;
184            Iterator<SyndEntry> it = entries.iterator();
185            
186            while (it.hasNext() && (length == -1 || index < length))
187            {
188                SyndEntry entry = it.next();
189                
190                atts = new AttributesImpl();
191                _addAttribute(atts, "title", entry.getTitle());
192                _addAttribute(atts, "link", entry.getLink());
193    
194                Date date = entry.getPublishedDate();
195                if (date != null)
196                {
197                    _addAttribute(atts, "date", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").format(date));
198                }
199    
200                XMLUtils.startElement(contentHandler, "entry", atts);
201                
202                SyndContent desc = entry.getDescription();
203                if (desc != null && desc.getValue() != null)
204                {
205                    atts = new AttributesImpl();
206                    _addAttribute(atts, "type", desc.getType());
207                    XMLUtils.createElement(contentHandler, "description", atts, desc.getValue());
208                }
209                
210                List<SyndPerson> authors = entry.getAuthors();
211                if (authors != null)
212                {
213                    for (SyndPerson author : authors)
214                    {
215                        atts = new AttributesImpl();
216                        _addAttribute(atts, "email", author.getEmail());
217                        XMLUtils.createElement(contentHandler, "author", atts, author.getName());
218                    }
219                }
220                
221                // Attachments
222                _saxEnclosures (entry);
223                
224                List<SyndCategory> entryCategories = entry.getCategories();
225                _saxCategories(entryCategories);
226                
227                _saxMediaRSS(entry);
228                
229                XMLUtils.endElement(contentHandler, "entry");
230                
231                index++;
232            }
233        }
234        
235        XMLUtils.endElement(contentHandler, "feed");
236    }
237
238    private void _saxCategories(List<SyndCategory> feedCategories) throws SAXException
239    {
240        for (SyndCategory category : feedCategories)
241        {
242            AttributesImpl categoryAtts = new AttributesImpl();
243            String taxonomyUri = category.getTaxonomyUri();
244            if (taxonomyUri != null)
245            {
246                _addAttribute(categoryAtts, "domain", taxonomyUri);
247            }
248            XMLUtils.createElement(contentHandler, "category", categoryAtts, category.getName());
249        }
250    }
251    
252    private void _saxEnclosures (SyndEntry entry) throws SAXException
253    {
254        List<SyndEnclosure> enclosures = entry.getEnclosures();
255        if (enclosures != null)
256        {
257            for (SyndEnclosure enclosure : enclosures)
258            {
259                AttributesImpl atts = new AttributesImpl();
260                _addAttribute(atts, "type", enclosure.getType());
261                _addAttribute(atts, "url", enclosure.getUrl());
262                _addAttribute(atts, "name", _enclosureName(enclosure.getUrl()));          
263                XMLUtils.startElement(contentHandler, "enclosure", atts);
264                _saxLength(enclosure.getLength());
265                XMLUtils.endElement(contentHandler, "enclosure");
266            }
267        }
268    }
269    
270    private void _saxMediaRSS(SyndEntry entry) throws SAXException
271    {
272        MediaEntryModule module = (MediaEntryModule) entry.getModule(MediaModule.URI);
273        if (module != null)
274        {
275            XMLUtils.startElement(contentHandler, "media");
276            
277            for (MediaContent content : module.getMediaContents())
278            {
279                _saxMediaContent(content, false);
280            }
281            
282            for (MediaGroup group : module.getMediaGroups())
283            {
284                AttributesImpl atts = new AttributesImpl();
285                _addAttribute(atts, "defaultContentIndex", group.getDefaultContentIndex());
286                XMLUtils.startElement(contentHandler, "mediagroup", atts);
287                
288                for (MediaContent content : group.getContents())
289                {
290                    _saxMediaContent(content, true);
291                }
292                
293                XMLUtils.endElement(contentHandler, "mediagroup");
294            }
295            
296            Metadata metadata = module.getMetadata();
297            
298            if (metadata != null)
299            {
300                saxMediaMetadata(metadata);
301            }
302            
303            XMLUtils.endElement(contentHandler, "media");
304        }
305    }
306    
307    private void _saxMediaContent(MediaContent content, boolean inGroup) throws SAXException
308    {
309        Metadata metadata = content.getMetadata();
310        Reference reference = content.getReference();
311        
312        AttributesImpl atts = new AttributesImpl();
313        
314        if (reference instanceof UrlReference)
315        {
316            _addAttribute(atts, "url", ((UrlReference) reference).getUrl().toString());
317        }
318        if (inGroup && content.isDefaultContent())
319        {
320            atts.addCDATAAttribute("default", "true");
321        }
322        _addAttribute(atts, "type", content.getType());
323        _addAttribute(atts, "medium", content.getMedium());
324        _addAttribute(atts, "size", content.getFileSize());
325        _addAttribute(atts, "width", content.getWidth());
326        _addAttribute(atts, "height", content.getHeight());
327        
328        XMLUtils.startElement(contentHandler, "mediacontent", atts);
329        
330        if (metadata != null)
331        {
332            saxMediaMetadata(metadata);
333        }
334        
335        XMLUtils.endElement(contentHandler, "mediacontent");
336    }
337    
338    private void saxMediaMetadata(Metadata metadata) throws SAXException
339    {
340        if (metadata != null)
341        {
342            String title = metadata.getTitle();
343            Thumbnail[] thumbnails = metadata.getThumbnail();
344            
345            if (title != null)
346            {
347                AttributesImpl titleAttrs = new AttributesImpl();
348                _addAttribute(titleAttrs, "type", metadata.getTitleType());
349                XMLUtils.createElement(contentHandler, "title", titleAttrs, title);
350            }
351            
352            for (Thumbnail thumbnail : thumbnails)
353            {
354                AttributesImpl thumbAttrs = new AttributesImpl();
355                _addAttribute(thumbAttrs, "url", thumbnail.getUrl().toString());
356                _addAttribute(thumbAttrs, "width", thumbnail.getWidth());
357                _addAttribute(thumbAttrs, "height", thumbnail.getHeight());
358                XMLUtils.createElement(contentHandler, "thumbnail", thumbAttrs);
359            }
360        }
361    }
362    
363    private boolean _checkForInfiniteLoop (String src)
364    {
365        Request request = ObjectModelHelper.getRequest(objectModel);
366        Page page = (Page) request.getAttribute(Page.class.getName());
367        
368        if (page == null)
369        {
370            return false;
371        }
372        
373        String cmsUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
374        Site site = page.getSite();
375        String siteName = page.getSiteName();
376        String lang = page.getSitemapName();
377        String path = page.getPathInSitemap();
378        
379        String frontUrl = site.getUrl() +  "/" + lang + "/" + path + ".html";
380        if (src.equals(frontUrl))
381        {
382            return true;
383        }
384        
385        String previewUrl = cmsUrl + "/preview/" + siteName + "/" + lang + "/" + path + ".html";
386        if (src.equals(previewUrl))
387        {
388            return true;
389        }
390        
391        String liveUrl = cmsUrl + "/live/" + siteName + "/" + lang + "/" + path + ".html";
392        if (src.equals(liveUrl))
393        {
394            return true;
395        }
396        
397        String backUrl = cmsUrl + siteName + "/" + lang + "/" + path + ".html";
398        if (src.equals(backUrl))
399        {
400            return true;
401        }
402        
403        return false;
404    }
405    
406    private String _enclosureName (String url)
407    {
408        String name = url;
409        if (name != null && name.lastIndexOf("/") > 0)
410        {
411            name = name.substring(name.lastIndexOf("/") + 1);
412        }
413        
414        if (name != null && name.indexOf("?") > 0)
415        {
416            name = name.substring(0, name.indexOf("?"));
417        }
418        
419        return name;
420    }
421    
422    private void _addAttribute(AttributesImpl atts, String name, Object value)
423    {
424        if (value != null)
425        {
426            if (value instanceof String)
427            {
428                atts.addCDATAAttribute(name, (String) value);
429            }
430            else if (value instanceof Integer)
431            {
432                atts.addCDATAAttribute(name, ((Integer) value).toString());
433            }
434            else if (value instanceof Long)
435            {
436                atts.addCDATAAttribute(name, ((Long) value).toString());
437            }
438            else if (value instanceof Double)
439            {
440                atts.addCDATAAttribute(name, ((Double) value).toString());
441            }
442            else if (value instanceof Float)
443            {
444                atts.addCDATAAttribute(name, ((Float) value).toString());
445            }
446        }
447    }
448    
449    private void _saxLength(long length) throws SAXException
450    {
451        org.ametys.core.util.StringUtils.toReadableDataSize(length).toSAX(contentHandler, "length");
452    }
453}