001/* 002 * Copyright 2019 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.plugins.userdirectory.service.search; 017 018import java.util.Collection; 019import java.util.Collections; 020import java.util.List; 021import java.util.Set; 022import java.util.function.Function; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.commons.collections4.SetUtils; 028import org.slf4j.Logger; 029import org.xml.sax.ContentHandler; 030import org.xml.sax.SAXException; 031 032import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 033import org.ametys.cms.contenttype.ContentTypesHelper; 034import org.ametys.cms.repository.Content; 035import org.ametys.cms.search.query.Query; 036import org.ametys.plugins.repository.AmetysObject; 037import org.ametys.plugins.userdirectory.UserDirectoryPageHandler; 038import org.ametys.plugins.userdirectory.page.UserDirectoryPageResolver; 039import org.ametys.plugins.userdirectory.page.UserPage; 040import org.ametys.web.frontoffice.search.instance.model.SearchContext; 041import org.ametys.web.frontoffice.search.instance.model.SiteContext; 042import org.ametys.web.frontoffice.search.instance.model.SitemapContext; 043import org.ametys.web.frontoffice.search.metamodel.AdditionalParameterValueMap; 044import org.ametys.web.frontoffice.search.metamodel.Returnable; 045import org.ametys.web.frontoffice.search.metamodel.ReturnableExtensionPoint; 046import org.ametys.web.frontoffice.search.metamodel.ReturnableSaxer; 047import org.ametys.web.frontoffice.search.metamodel.impl.PageReturnable; 048import org.ametys.web.frontoffice.search.metamodel.impl.PageSaxer; 049import org.ametys.web.frontoffice.search.metamodel.impl.PrivateContentReturnable; 050import org.ametys.web.frontoffice.search.requesttime.SearchComponentArguments; 051import org.ametys.web.repository.page.Page; 052import org.ametys.web.repository.site.Site; 053import org.ametys.web.search.query.ContentPageQuery; 054 055/** 056 * The returnable for User 057 */ 058public class UserReturnable extends PrivateContentReturnable 059{ 060 /** The returnable for pages. Only used because {@link UserSaxer} extends {@link PageSaxer} whose constructor needs it. */ 061 protected PageReturnable _pageReturnable; 062 /** The helper for content types */ 063 protected ContentTypesHelper _contentTypeHelper; 064 /** The content type extension point */ 065 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 066 /** The resolver for user directory virtual pages */ 067 protected UserDirectoryPageResolver _userDirectoryPageResolver; 068 069 @Override 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 super.service(manager); 073 ReturnableExtensionPoint returnableEP = (ReturnableExtensionPoint) manager.lookup(ReturnableExtensionPoint.ROLE); 074 _pageReturnable = (PageReturnable) returnableEP.getExtension(PageReturnable.ROLE); 075 _contentTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 076 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 077 _userDirectoryPageResolver = (UserDirectoryPageResolver) manager.lookup(UserDirectoryPageResolver.ROLE); 078 } 079 080 @Override 081 protected Function<Query, Query> siteQueryJoiner() 082 { 083 // User contents are created through plugin content-io, and are "off-site". 084 // When selecting a site context, it should search that a linked page exist in the selected site 085 return ContentPageQuery::new; 086 } 087 088 @Override 089 public ReturnableSaxer getSaxer(Collection<Returnable> allReturnables, AdditionalParameterValueMap additionalParameterValues) 090 { 091 return new UserSaxer(additionalParameterValues); 092 } 093 094 /** 095 * The {@link ReturnableSaxer} for user contents. 096 * <br>Their pages are SAXed. 097 */ 098 public class UserSaxer extends PageSaxer 099 { 100 /** The user content types selected in the additional parameter of the current servicee instance (and their descendants) */ 101 protected Collection<String> _allContentTypes; 102 103 /** 104 * Constructor 105 * @param additionalParameterValues The additional parameter values 106 */ 107 public UserSaxer(AdditionalParameterValueMap additionalParameterValues) 108 { 109 super(UserReturnable.this._pageReturnable); 110 _allContentTypes = _allContentTypeIds(additionalParameterValues); 111 } 112 113 @SuppressWarnings("synthetic-access") 114 private Collection<String> _currentServiceInstanceContentTypes(AdditionalParameterValueMap additionalParameterValues) 115 { 116 return UserReturnable.this.getContentTypes(additionalParameterValues); 117 } 118 119 private Collection<String> _allContentTypeIds(AdditionalParameterValueMap additionalParameterValues) 120 { 121 return _currentServiceInstanceContentTypes(additionalParameterValues) 122 .stream() 123 .map(this::_selfAndDescendantContentTypes) 124 .flatMap(Set::stream) 125 .collect(Collectors.toList()); 126 } 127 128 private Set<String> _selfAndDescendantContentTypes(String cType) 129 { 130 return SetUtils.union( 131 Collections.singleton(cType), 132 _contentTypeExtensionPoint.getSubTypes(cType)); 133 } 134 135 @Override 136 public String getIdentifier() 137 { 138 return PageSaxer.class.getName(); 139 } 140 141 @Override 142 public boolean canSax(AmetysObject hit, Logger logger, SearchComponentArguments args) 143 { 144 return hit instanceof Content 145 && _contentTypeHelper.isInstanceOf((Content) hit, UserDirectoryPageHandler.ABSTRACT_USER_CONTENT_TYPE); 146 } 147 148 @Override 149 public void sax(ContentHandler contentHandler, AmetysObject hit, Logger logger, SearchComponentArguments args) throws SAXException 150 { 151 Content userContent = (Content) hit; 152 SearchContext searchContext = _getSearchContext(args); 153 String siteName = _getSiteName(args, searchContext); 154 String sitemapName = _getSitemapName(args, searchContext, userContent); 155 UserPage userPage = _resolveUserPage(userContent, siteName, sitemapName); 156 _saxUserPage(contentHandler, userPage, logger, args); 157 } 158 159 /** 160 * Gets the search context to use to determine site and sitemap as to resolve user pages from user contents 161 * @param args The search arguments 162 * @return The search context 163 */ 164 protected SearchContext _getSearchContext(SearchComponentArguments args) 165 { 166 // If several contexts are selected with different sites, the behavior would probably not be the expected one 167 // Only the pages of the first site would be SAXed 168 // As a result we do not allow this configuration 169 // The practical case is to select only one site, so this is not a restriction too much strict 170 Collection<SearchContext> searchContexts = args.serviceInstance().getContexts(); 171 if (searchContexts.size() != 1) 172 { 173 throw new IllegalStateException("A search context is mandatory with UserReturnable in order to resolve the corresponding user pages. Multiple contexts are not allowed too."); 174 } 175 return searchContexts.iterator().next(); 176 } 177 178 /** 179 * Gets the site name 180 * @param args The search arguments 181 * @param searchContext The search context 182 * @return the site name 183 */ 184 protected String _getSiteName(SearchComponentArguments args, SearchContext searchContext) 185 { 186 SiteContext siteContext = searchContext.siteContext(); 187 switch (siteContext.getType()) 188 { 189 case CURRENT: 190 return args.currentSite().getName(); 191 case AMONG: 192 List<Site> sites = siteContext.getSites().get(); 193 if (sites.size() == 1) 194 { 195 return sites.iterator().next().getName(); 196 } 197 throw new IllegalStateException("Site context must select one and only one site."); 198 case ALL: 199 case OTHERS: 200 default: 201 throw new IllegalStateException("Site context must select one and only one site."); 202 } 203 } 204 205 /** 206 * Gets the sitemap name 207 * @param args The search arguments 208 * @param searchContext The search context 209 * @param userContent The user content (current hit) 210 * @return the sitemap name 211 */ 212 protected String _getSitemapName(SearchComponentArguments args, SearchContext searchContext, Content userContent) 213 { 214 SitemapContext sitemapContext = searchContext.sitemapContext(); 215 switch (sitemapContext.getType()) 216 { 217 case CURRENT_SITE: 218 String contentLanguage = userContent.getLanguage(); 219 return contentLanguage == null 220 ? args.currentPage().getSitemapName() 221 : contentLanguage; 222 case CHILD_PAGES: 223 case DIRECT_CHILD_PAGES: 224 return args.currentPage().getSitemapName(); 225 case CHILD_PAGES_OF: 226 case DIRECT_CHILD_PAGES_OF: 227 List<Page> pages = sitemapContext.getPages().get(); 228 long nbSites = pages.stream() 229 .map(Page::getSite) 230 .distinct() 231 .count(); 232 long nbSitemaps = pages.stream() 233 .map(Page::getSitemap) 234 .distinct() 235 .count(); 236 if (nbSites != 1 || nbSitemaps != 1) 237 { 238 throw new IllegalStateException("Sitemap context must select one and only one sitemap."); 239 } 240 return pages.iterator().next().getSitemapName(); 241 default: 242 throw new IllegalStateException("Sitemap context must select one and only one sitemap."); 243 } 244 } 245 246 /** 247 * Resolves the use page corresponding to the user content 248 * @param userContent The user content 249 * @param siteName The site name 250 * @param sitemapName The sitemap name 251 * @return The user page 252 */ 253 protected UserPage _resolveUserPage(Content userContent, String siteName, String sitemapName) 254 { 255 for (String contentType : _allContentTypes) 256 { 257 UserPage userPage = _userDirectoryPageResolver.getUserPage(userContent, siteName, sitemapName, contentType); 258 if (userPage != null) 259 { 260 return userPage; 261 } 262 } 263 264 String msg = String.format("The page associated to the user content %s cannot be found in the current site and sitemap (%s / %s) for one of the content types %s", 265 userContent, 266 siteName, 267 sitemapName, 268 _allContentTypes); 269 throw new IllegalArgumentException(msg); 270 } 271 272 /** 273 * Sax the user page 274 * @param contentHandler The content handler 275 * @param userPage The user page 276 * @param logger The logger 277 * @param args The search arguments 278 * @throws SAXException if a SAX error occured 279 */ 280 protected void _saxUserPage(ContentHandler contentHandler, UserPage userPage, Logger logger, SearchComponentArguments args) throws SAXException 281 { 282 super.sax(contentHandler, userPage, logger, args); 283 } 284 } 285}