001/*
002 *  Copyright 2018 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.web.frontoffice.search.requesttime.impl;
017
018import java.io.IOException;
019import java.time.ZonedDateTime;
020import java.time.format.DateTimeFormatter;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Locale;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.cocoon.components.source.impl.SitemapSource;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.environment.Response;
031import org.apache.cocoon.xml.XMLUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.excalibur.source.SourceResolver;
034import org.slf4j.Logger;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.SAXException;
037
038import org.ametys.cms.repository.Content;
039import org.ametys.cms.search.SearchResult;
040import org.ametys.cms.search.SearchResults;
041import org.ametys.core.util.IgnoreRootHandler;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.web.frontoffice.search.instance.SearchServiceInstance;
044import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode;
045import org.ametys.web.frontoffice.search.metamodel.impl.ContentReturnable;
046import org.ametys.web.frontoffice.search.requesttime.SearchComponent;
047import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
048import org.ametys.web.renderingcontext.RenderingContext;
049import org.ametys.web.renderingcontext.RenderingContextHandler;
050import org.ametys.web.repository.site.Site;
051
052/**
053 * {@link SearchComponent} for saxing results for RSS feed
054 */
055public class RssSearchComponent implements SearchComponent, Serviceable
056{
057    private static final String __ENABLE_RSS_PARAMETER_NAME = "rss";
058    private static final String __RSS_VIEW_PARAMETER_NAME = "rssView";
059    private static final String __RSS_VIEW_PARAMETER_DEFAULT_VALUE = "abstract";
060    private SourceResolver _resolver;
061    private RenderingContextHandler _renderingContextHandler;
062    
063    @Override
064    public void service(ServiceManager manager) throws ServiceException
065    {
066        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
067        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
068    }
069    
070    @Override
071    public int priority()
072    {
073        return MIN_PRIORITY - 500;
074    }
075
076    @Override
077    public boolean supports(SearchComponentArguments args)
078    {
079        return args.generatorParameters().getParameterAsBoolean(__ENABLE_RSS_PARAMETER_NAME, false);
080    }
081
082    @Override
083    public void execute(SearchComponentArguments args) throws Exception
084    {
085        SearchResults<AmetysObject> results = args.results().orElseThrow(() -> new IllegalStateException("Results have not been set yet."));
086        ContentHandler contentHandler = args.contentHandler();
087        String view = args.generatorParameters().getParameter(__RSS_VIEW_PARAMETER_NAME, _getRssViewParameterDefaultValue(args));
088        Site site = args.currentSite();
089        Logger logger = args.logger();
090        SearchServiceInstance serviceInstance = args.serviceInstance();
091        String title = serviceInstance.getTitle();
092        
093        _setRssHeader(serviceInstance, args.response());
094        _setSiteAttribute(args.request(), site);
095        _saxRssFeed(contentHandler, title, results, view, site, logger);
096    }
097    
098    private String _getRssViewParameterDefaultValue(SearchComponentArguments args)
099    {
100        String view = args.serviceInstance().getAdditionalParameterValues().getValue(ContentReturnable.PARAMETER_VIEW);
101        return view == null ? __RSS_VIEW_PARAMETER_DEFAULT_VALUE : view;
102    }
103    
104    private void _setRssHeader(SearchServiceInstance serviceInstance, Response response)
105    {
106        boolean isCacheable = serviceInstance.getRightCheckingMode() == RightCheckingMode.FAST;
107        if (isCacheable)
108        {
109            response.setHeader("X-Ametys-Cacheable", "true");
110        }
111    }
112    
113    private void _setSiteAttribute(Request request, Site currentSite)
114    {
115        final String siteAttrName = "site";
116        if (request.getAttribute(siteAttrName) == null)
117        {
118            request.setAttribute(siteAttrName, currentSite.getName());
119        }
120    }
121    
122    private void _saxRssFeed(ContentHandler contentHandler, String title, SearchResults<AmetysObject> results, String view, Site site, Logger logger) throws Exception
123    {
124        XMLUtils.startElement(contentHandler, "RssFeed");
125        
126        if (StringUtils.isNotEmpty(title))
127        {
128            XMLUtils.createElement(contentHandler, "title", title);
129        }
130        
131        DateTimeFormatter dtfRFC = DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z").withLocale(Locale.ENGLISH);
132        XMLUtils.createElement(contentHandler, "pubDate", dtfRFC.format(ZonedDateTime.now()));
133        
134        List<String> noContentResults = new ArrayList<>();
135        for (SearchResult<AmetysObject> searchResult :  results.getResults())
136        {
137            AmetysObject object = searchResult.getObject();
138            if (object instanceof Content)
139            {
140                _saxContentItem(contentHandler, (Content) object, view, site.getName());
141            }
142            else
143            {
144                noContentResults.add(object.getId());
145            }
146        }
147        if (!noContentResults.isEmpty())
148        {
149            logger.warn("{}\n could not be SAXed. Search results which are not Contents are not yet handled by RSS.", noContentResults);
150        }
151        
152        XMLUtils.endElement(contentHandler, "RssFeed");
153    }
154    
155    private void _saxContentItem(ContentHandler contentHandler, Content content, String view, String siteName) throws SAXException, IOException
156    {
157        RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
158        SitemapSource src = null;
159        try
160        {
161            if (currentContext == RenderingContext.BACK)
162            {
163                // Issue CMS-2642
164                _renderingContextHandler.setRenderingContext(RenderingContext.PREVIEW);
165            }
166            
167            String uri = "cocoon:/_content.rss?contentId=" + content.getId() + "&viewName=" + view + "&siteName=" + siteName;
168        
169            src = (SitemapSource) _resolver.resolveURI(uri);
170            src.toSAX(new IgnoreRootHandler(contentHandler));
171        }
172        finally
173        {
174            if (src != null)
175            {
176                _resolver.release(src);
177            }
178            _renderingContextHandler.setRenderingContext(currentContext);
179        }
180    }
181}