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.metamodel.impl;
017
018import java.io.InputStream;
019import java.time.ZonedDateTime;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Optional;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.excalibur.xml.sax.SAXParser;
034import org.slf4j.Logger;
035import org.xml.sax.ContentHandler;
036import org.xml.sax.InputSource;
037import org.xml.sax.SAXException;
038
039import org.ametys.cms.content.RichTextHandler;
040import org.ametys.cms.data.RichText;
041import org.ametys.cms.data.type.ModelItemTypeConstants;
042import org.ametys.cms.repository.Content;
043import org.ametys.core.util.DateUtils;
044import org.ametys.core.util.LambdaUtils;
045import org.ametys.plugins.repository.AmetysObject;
046import org.ametys.plugins.repository.AmetysObjectIterable;
047import org.ametys.plugins.repository.AmetysRepositoryException;
048import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
049import org.ametys.runtime.model.View;
050import org.ametys.runtime.model.type.DataContext;
051import org.ametys.web.frontoffice.search.metamodel.ReturnableSaxer;
052import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments;
053import org.ametys.web.repository.page.Page;
054import org.ametys.web.repository.page.Page.PageType;
055import org.ametys.web.repository.page.Zone;
056import org.ametys.web.repository.page.ZoneItem;
057import org.ametys.web.repository.page.ZoneItem.ZoneType;
058import org.ametys.web.repository.site.Site;
059
060/**
061 * {@link ReturnableSaxer} for {@link PageReturnable}
062 */
063public class PageSaxer implements ReturnableSaxer
064{
065    /** The associated returnable on pages */
066    protected PageReturnable _pageReturnable;
067    
068    /**
069     * Constructor
070     * @param pageReturnable The associated returnable on pages
071     */
072    public PageSaxer(PageReturnable pageReturnable)
073    {
074        _pageReturnable = pageReturnable;
075    }
076    
077    @Override
078    public boolean canSax(AmetysObject hit, Logger logger, SearchComponentArguments args)
079    {
080        return hit instanceof Page;
081    }
082    
083    @Override
084    public void sax(ContentHandler contentHandler, AmetysObject hit, Logger logger, SearchComponentArguments args) throws SAXException
085    {
086        Page page = (Page) hit;
087        XMLUtils.createElement(contentHandler, "title", page.getTitle());
088        _saxPageContents(page, contentHandler, logger);
089        XMLUtils.createElement(contentHandler, "type", "page");
090        XMLUtils.createElement(contentHandler, "uri", page.getSitemap().getName() + "/" + page.getPathInSitemap());
091        
092        _saxLastModifiedDate(page, contentHandler);
093        _saxLastValidationDate(page, contentHandler);
094        
095        String siteName = page.getSiteName();
096        if (siteName != null)
097        {
098            Site site = _pageReturnable._siteManager.getSite(siteName);
099            XMLUtils.createElement(contentHandler, "siteName", siteName);
100            XMLUtils.createElement(contentHandler, "siteTitle", site.getTitle());
101            Optional.ofNullable(site.getUrl()).ifPresent(LambdaUtils.wrapConsumer(url -> XMLUtils.createElement(contentHandler, "siteUrl", url)));
102        }
103    }
104    
105    private void _saxPageContents(Page page, ContentHandler handler, Logger logger)
106    {
107        Locale locale = new Locale(page.getSitemapName());
108        _pageContents(page).forEach(LambdaUtils.wrapConsumer(content -> saxContent(content, "index", locale, handler, logger)));
109    }
110    
111    private void _saxLastModifiedDate(Page page, ContentHandler handler) throws SAXException
112    {
113        _saxLastDate(page, Content::getLastModified, handler, "lastModified");
114    }
115    
116    private void _saxLastValidationDate(Page page, ContentHandler handler) throws SAXException
117    {
118        _saxLastDate(page, Content::getLastValidationDate, handler, "lastValidation");
119    }
120    
121    private void _saxLastDate(Page page, Function<Content, ZonedDateTime> dateRetriever, ContentHandler handler, String tagName) throws SAXException
122    {
123        ZonedDateTime lastDate = null;
124        List<Content> pageContents = _pageContents(page).collect(Collectors.toList());
125
126        for (Content content : pageContents)
127        {
128            ZonedDateTime contentDate = dateRetriever.apply(content);
129            if (contentDate != null && (lastDate == null || contentDate.isAfter(lastDate)))
130            {
131                // Keep the latest date
132                lastDate = contentDate;
133            }
134        }
135        
136        if (lastDate != null)
137        {
138            XMLUtils.createElement(handler, tagName, DateUtils.zonedDateTimeToString(lastDate));
139        }
140    }
141    
142    private static Stream<Content> _pageContents(Page page)
143    {
144        return page != null && page.getType() == PageType.CONTAINER 
145                ? page.getZones()
146                    .stream()
147                    .map(Zone::getZoneItems)
148                    .flatMap(AmetysObjectIterable::stream)
149                    .filter(zi -> zi.getType() == ZoneType.CONTENT)
150                    .map(ZoneItem::getContent)
151                    .map(Content.class::cast)
152                : Stream.empty();
153    }
154    
155    /**
156     * SAX the view of a content if exists
157     * @param content the content 
158     * @param viewName The name of view to sax
159     * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. Only use if initial content's language is not null.
160     * @param contentHandler The content handler
161     * @param logger The logger
162     * @throws SAXException if an exception occurs while saxing
163     */
164    protected void saxContent(Content content, String viewName, Locale defaultLocale, ContentHandler contentHandler, Logger logger) throws SAXException
165    {
166        try
167        {
168            XMLUtils.createElement(contentHandler, "contentName", content.getName());
169            
170            String[] contentTypes = content.getTypes();
171            XMLUtils.startElement(contentHandler, "contentTypes");
172            Arrays.asList(contentTypes).forEach(LambdaUtils.wrapConsumer(cType -> XMLUtils.createElement(contentHandler, "contentType", cType)));
173            XMLUtils.endElement(contentHandler, "contentTypes");
174            
175            View view = _pageReturnable._contentTypesHelper.getView(viewName, contentTypes, content.getMixinTypes());
176            if (view != null)
177            {
178                Map<String, Object> richTexts = DataHolderHelper.findItemsByType(content, view, ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID);
179                
180                String contentId = content.getId();
181                for (Entry<String, Object> richText : richTexts.entrySet())
182                {
183                    String dataPath = richText.getKey();
184                    Object value = richText.getValue();
185                    
186                    if (value instanceof Collection<?>)
187                    {
188                        for (Object v : (Collection<?>) value)
189                        {
190                            saxRichTextExcerpt(dataPath, contentId, v, contentHandler, logger);
191                        }
192                    }
193                    else
194                    {
195                        saxRichTextExcerpt(dataPath, contentId, value, contentHandler, logger);
196                    }
197                }
198                
199                AttributesImpl attrs = new AttributesImpl();
200                attrs.addCDATAAttribute("id", content.getId());
201                attrs.addCDATAAttribute("name", content.getName());
202                Optional.ofNullable(content.getLanguage()).ifPresent(lang -> attrs.addCDATAAttribute("language", lang));
203                
204                XMLUtils.startElement(contentHandler, "content", attrs);
205                content.dataToSAX(contentHandler, view, DataContext.newInstance().withLocale(defaultLocale).withEmptyValues(false));
206                XMLUtils.endElement(contentHandler, "content");
207            }
208        }
209        catch (AmetysRepositoryException e)
210        {
211            logger.error("Cannot sax information about the content {}", content.getId(), e);
212        }
213    }
214    
215    /**
216     * SAX excerpt for rich text
217     * @param path The richText path
218     * @param contentId The content id
219     * @param richText The rich text
220     * @param contentHandler The content handler
221     * @param logger The logger
222     */
223    protected void saxRichTextExcerpt(String path, String contentId, Object richText, ContentHandler contentHandler, Logger logger)
224    {
225        if (richText instanceof RichText)
226        {
227            SAXParser saxParser = null;
228            try (InputStream is = ((RichText) richText).getInputStream())
229            {
230                RichTextHandler txtHandler = new RichTextHandler(200);
231                saxParser = (SAXParser) _pageReturnable._manager.lookup(SAXParser.ROLE);
232                saxParser.parse(new InputSource(is), txtHandler);
233                String textValue = txtHandler.getValue();
234                if (textValue != null)
235                {
236                    XMLUtils.createElement(contentHandler, "excerpt", textValue);
237                }
238            }
239            catch (Exception e)
240            {
241                logger.error("Cannot convert a richtext value at path '{}' of content '{}'", path, contentId, e);
242            }
243            finally
244            {
245                _pageReturnable._manager.release(saxParser);
246            }
247        }
248    }
249}