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}