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