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}