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}