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}