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.Map.Entry;
027import java.util.Optional;
028import java.util.function.Function;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.excalibur.xml.sax.SAXParser;
035import org.slf4j.Logger;
036import org.xml.sax.ContentHandler;
037import org.xml.sax.InputSource;
038import org.xml.sax.SAXException;
039
040import org.ametys.cms.content.RichTextHandler;
041import org.ametys.cms.data.RichText;
042import org.ametys.cms.data.type.ModelItemTypeConstants;
043import org.ametys.cms.repository.Content;
044import org.ametys.core.util.DateUtils;
045import org.ametys.core.util.LambdaUtils;
046import org.ametys.plugins.repository.AmetysObject;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
050import org.ametys.runtime.model.View;
051import org.ametys.runtime.model.type.DataContext;
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            View view = _pageReturnable._contentTypesHelper.getView(metadataSetName, contentTypes, content.getMixinTypes());
177            if (view != null)
178            {
179                Map<String, Object> richTexts = DataHolderHelper.findItemsByType(content, view, ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID);
180                
181                String contentId = content.getId();
182                for (Entry<String, Object> richText : richTexts.entrySet())
183                {
184                    String dataPath = richText.getKey();
185                    Object value = richText.getValue();
186                    
187                    if (value instanceof Collection<?>)
188                    {
189                        for (Object v : (Collection<?>) value)
190                        {
191                            saxRichTextExcerpt(dataPath, contentId, v, contentHandler, logger);
192                        }
193                    }
194                    else
195                    {
196                        saxRichTextExcerpt(dataPath, contentId, value, contentHandler, logger);
197                    }
198                }
199                
200                AttributesImpl attrs = new AttributesImpl();
201                attrs.addCDATAAttribute("id", content.getId());
202                attrs.addCDATAAttribute("name", content.getName());
203                Optional.ofNullable(content.getLanguage()).ifPresent(lang -> attrs.addCDATAAttribute("language", lang));
204                
205                XMLUtils.startElement(contentHandler, "content", attrs);
206                content.dataToSAX(contentHandler, view, DataContext.newInstance().withLocale(defaultLocale).withEmptyValues(false));
207                XMLUtils.endElement(contentHandler, "content");
208            }
209        }
210        catch (AmetysRepositoryException | IOException e)
211        {
212            logger.error("Cannot sax information about the content {}", content.getId(), e);
213        }
214    }
215    
216    /**
217     * SAX excerpt for rich text
218     * @param path The richText path
219     * @param contentId The content id
220     * @param richText The rich text
221     * @param contentHandler The content handler
222     * @param logger The logger
223     */
224    protected void saxRichTextExcerpt(String path, String contentId, Object richText, ContentHandler contentHandler, Logger logger)
225    {
226        if (richText instanceof RichText)
227        {
228            SAXParser saxParser = null;
229            try (InputStream is = ((RichText) richText).getInputStream())
230            {
231                RichTextHandler txtHandler = new RichTextHandler(200);
232                saxParser = (SAXParser) _pageReturnable._manager.lookup(SAXParser.ROLE);
233                saxParser.parse(new InputSource(is), txtHandler);
234                String textValue = txtHandler.getValue();
235                if (textValue != null)
236                {
237                    XMLUtils.createElement(contentHandler, "excerpt", textValue);
238                }
239            }
240            catch (Exception e)
241            {
242                logger.error("Cannot convert a richtext value at path '{}' of content '{}'", path, contentId, e);
243            }
244            finally
245            {
246                _pageReturnable._manager.release(saxParser);
247            }
248        }
249    }
250}