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.core.util.HttpUtils;
037import org.ametys.runtime.config.Config;
038import org.ametys.web.renderingcontext.RenderingContext;
039import org.ametys.web.renderingcontext.RenderingContextHandler;
040import org.ametys.web.repository.page.Page;
041import org.ametys.web.repository.site.Site;
042
043import com.rometools.modules.mediarss.MediaEntryModule;
044import com.rometools.modules.mediarss.MediaModule;
045import com.rometools.modules.mediarss.types.MediaContent;
046import com.rometools.modules.mediarss.types.MediaGroup;
047import com.rometools.modules.mediarss.types.Metadata;
048import com.rometools.modules.mediarss.types.Reference;
049import com.rometools.modules.mediarss.types.Thumbnail;
050import com.rometools.modules.mediarss.types.UrlReference;
051import com.rometools.rome.feed.synd.SyndCategory;
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        List<SyndCategory> feedCategories = feed.getCategories();
179        _saxCategories(feedCategories);
180        
181        List<SyndEntry> entries = feed.getEntries();
182        if (entries != null)
183        {
184            int index = 0;
185            Iterator<SyndEntry> it = entries.iterator();
186            
187            while (it.hasNext() && (length == -1 || index < length))
188            {
189                SyndEntry entry = it.next();
190                
191                atts = new AttributesImpl();
192                _addAttribute(atts, "title", entry.getTitle());
193                _addAttribute(atts, "link", entry.getLink());
194    
195                Date date = entry.getPublishedDate();
196                if (date != null)
197                {
198                    _addAttribute(atts, "date", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm").format(date));
199                }
200    
201                XMLUtils.startElement(contentHandler, "entry", atts);
202                
203                SyndContent desc = entry.getDescription();
204                if (desc != null && desc.getValue() != null)
205                {
206                    atts = new AttributesImpl();
207                    _addAttribute(atts, "type", desc.getType());
208                    XMLUtils.createElement(contentHandler, "description", atts, desc.getValue());
209                }
210                
211                List<SyndPerson> authors = entry.getAuthors();
212                if (authors != null)
213                {
214                    for (SyndPerson author : authors)
215                    {
216                        atts = new AttributesImpl();
217                        _addAttribute(atts, "email", author.getEmail());
218                        XMLUtils.createElement(contentHandler, "author", atts, author.getName());
219                    }
220                }
221                
222                // Attachments
223                _saxEnclosures (entry);
224                
225                List<SyndCategory> entryCategories = entry.getCategories();
226                _saxCategories(entryCategories);
227                
228                _saxMediaRSS(entry);
229                
230                XMLUtils.endElement(contentHandler, "entry");
231                
232                index++;
233            }
234        }
235        
236        XMLUtils.endElement(contentHandler, "feed");
237    }
238
239    private void _saxCategories(List<SyndCategory> feedCategories) throws SAXException
240    {
241        for (SyndCategory category : feedCategories)
242        {
243            AttributesImpl categoryAtts = new AttributesImpl();
244            String taxonomyUri = category.getTaxonomyUri();
245            if (taxonomyUri != null)
246            {
247                _addAttribute(categoryAtts, "domain", taxonomyUri);
248            }
249            XMLUtils.createElement(contentHandler, "category", categoryAtts, category.getName());
250        }
251    }
252    
253    private void _saxEnclosures (SyndEntry entry) throws SAXException
254    {
255        List<SyndEnclosure> enclosures = entry.getEnclosures();
256        if (enclosures != null)
257        {
258            for (SyndEnclosure enclosure : enclosures)
259            {
260                AttributesImpl atts = new AttributesImpl();
261                _addAttribute(atts, "type", enclosure.getType());
262                _addAttribute(atts, "url", enclosure.getUrl());
263                _addAttribute(atts, "name", _enclosureName(enclosure.getUrl()));
264                XMLUtils.startElement(contentHandler, "enclosure", atts);
265                _saxLength(enclosure.getLength());
266                XMLUtils.endElement(contentHandler, "enclosure");
267            }
268        }
269    }
270    
271    private void _saxMediaRSS(SyndEntry entry) throws SAXException
272    {
273        MediaEntryModule module = (MediaEntryModule) entry.getModule(MediaModule.URI);
274        if (module != null)
275        {
276            XMLUtils.startElement(contentHandler, "media");
277            
278            for (MediaContent content : module.getMediaContents())
279            {
280                _saxMediaContent(content, false);
281            }
282            
283            for (MediaGroup group : module.getMediaGroups())
284            {
285                AttributesImpl atts = new AttributesImpl();
286                _addAttribute(atts, "defaultContentIndex", group.getDefaultContentIndex());
287                XMLUtils.startElement(contentHandler, "mediagroup", atts);
288                
289                for (MediaContent content : group.getContents())
290                {
291                    _saxMediaContent(content, true);
292                }
293                
294                XMLUtils.endElement(contentHandler, "mediagroup");
295            }
296            
297            Metadata metadata = module.getMetadata();
298            
299            if (metadata != null)
300            {
301                saxMediaMetadata(metadata);
302            }
303            
304            XMLUtils.endElement(contentHandler, "media");
305        }
306    }
307    
308    private void _saxMediaContent(MediaContent content, boolean inGroup) throws SAXException
309    {
310        Metadata metadata = content.getMetadata();
311        Reference reference = content.getReference();
312        
313        AttributesImpl atts = new AttributesImpl();
314        
315        if (reference instanceof UrlReference)
316        {
317            _addAttribute(atts, "url", ((UrlReference) reference).getUrl().toString());
318        }
319        if (inGroup && content.isDefaultContent())
320        {
321            atts.addCDATAAttribute("default", "true");
322        }
323        _addAttribute(atts, "type", content.getType());
324        _addAttribute(atts, "medium", content.getMedium());
325        _addAttribute(atts, "size", content.getFileSize());
326        _addAttribute(atts, "width", content.getWidth());
327        _addAttribute(atts, "height", content.getHeight());
328        
329        XMLUtils.startElement(contentHandler, "mediacontent", atts);
330        
331        if (metadata != null)
332        {
333            saxMediaMetadata(metadata);
334        }
335        
336        XMLUtils.endElement(contentHandler, "mediacontent");
337    }
338    
339    private void saxMediaMetadata(Metadata metadata) throws SAXException
340    {
341        if (metadata != null)
342        {
343            String title = metadata.getTitle();
344            Thumbnail[] thumbnails = metadata.getThumbnail();
345            
346            if (title != null)
347            {
348                AttributesImpl titleAttrs = new AttributesImpl();
349                _addAttribute(titleAttrs, "type", metadata.getTitleType());
350                XMLUtils.createElement(contentHandler, "title", titleAttrs, title);
351            }
352            
353            for (Thumbnail thumbnail : thumbnails)
354            {
355                AttributesImpl thumbAttrs = new AttributesImpl();
356                _addAttribute(thumbAttrs, "url", thumbnail.getUrl().toString());
357                _addAttribute(thumbAttrs, "width", thumbnail.getWidth());
358                _addAttribute(thumbAttrs, "height", thumbnail.getHeight());
359                XMLUtils.createElement(contentHandler, "thumbnail", thumbAttrs);
360            }
361        }
362    }
363    
364    private boolean _checkForInfiniteLoop (String src)
365    {
366        Request request = ObjectModelHelper.getRequest(objectModel);
367        Page page = (Page) request.getAttribute(Page.class.getName());
368        
369        if (page == null)
370        {
371            return false;
372        }
373        
374        String cmsUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url"));
375        Site site = page.getSite();
376        String siteName = page.getSiteName();
377        String lang = page.getSitemapName();
378        String path = page.getPathInSitemap();
379        
380        String frontUrl = site.getUrl() +  "/" + lang + "/" + path + ".html";
381        if (src.equals(frontUrl))
382        {
383            return true;
384        }
385        
386        String previewUrl = cmsUrl + "/preview/" + siteName + "/" + lang + "/" + path + ".html";
387        if (src.equals(previewUrl))
388        {
389            return true;
390        }
391        
392        String liveUrl = cmsUrl + "/live/" + siteName + "/" + lang + "/" + path + ".html";
393        if (src.equals(liveUrl))
394        {
395            return true;
396        }
397        
398        String backUrl = cmsUrl + siteName + "/" + lang + "/" + path + ".html";
399        if (src.equals(backUrl))
400        {
401            return true;
402        }
403        
404        return false;
405    }
406    
407    private String _enclosureName (String url)
408    {
409        String name = url;
410        if (name != null && name.lastIndexOf("/") > 0)
411        {
412            name = name.substring(name.lastIndexOf("/") + 1);
413        }
414        
415        if (name != null && name.indexOf("?") > 0)
416        {
417            name = name.substring(0, name.indexOf("?"));
418        }
419        
420        return name;
421    }
422    
423    private void _addAttribute(AttributesImpl atts, String name, Object value)
424    {
425        if (value != null)
426        {
427            if (value instanceof String)
428            {
429                atts.addCDATAAttribute(name, (String) value);
430            }
431            else if (value instanceof Integer)
432            {
433                atts.addCDATAAttribute(name, ((Integer) value).toString());
434            }
435            else if (value instanceof Long)
436            {
437                atts.addCDATAAttribute(name, ((Long) value).toString());
438            }
439            else if (value instanceof Double)
440            {
441                atts.addCDATAAttribute(name, ((Double) value).toString());
442            }
443            else if (value instanceof Float)
444            {
445                atts.addCDATAAttribute(name, ((Float) value).toString());
446            }
447        }
448    }
449    
450    private void _saxLength(long length) throws SAXException
451    {
452        org.ametys.core.util.StringUtils.toReadableDataSize(length).toSAX(contentHandler, "length");
453    }
454}