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