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