001/*
002 *  Copyright 2020 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.workspaces.search;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026import java.util.function.Function;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.ProcessingException;
032import org.apache.cocoon.components.source.impl.SitemapSource;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.generation.ServiceableGenerator;
036import org.apache.cocoon.xml.AttributesImpl;
037import org.apache.cocoon.xml.XMLUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.excalibur.source.SourceResolver;
040import org.xml.sax.SAXException;
041
042import org.ametys.core.right.RightManager;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.util.IgnoreRootHandler;
046import org.ametys.plugins.workspaces.categories.Category;
047import org.ametys.plugins.workspaces.categories.CategoryHelper;
048import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint;
049import org.ametys.plugins.workspaces.members.ProjectMemberManager;
050import org.ametys.plugins.workspaces.project.ProjectManager;
051import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
052import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
053import org.ametys.plugins.workspaces.project.objects.Project;
054
055/**
056 * Generator for modular search service
057 *
058 */
059public class ModularSearchGenerator extends ServiceableGenerator
060{
061    /** Search Module Extension Point */
062    protected SearchModuleExtensionPoint _searchModuleEP;
063    
064    /** Source Resolver */
065    protected SourceResolver _sourceResolver;
066
067    /** The project manager */
068    protected ProjectManager _projectManager;
069    
070    /** The current user provider */
071    protected CurrentUserProvider _currentUserProvider;
072
073    /** The project member manager */
074    protected ProjectMemberManager _projectMemberManager;
075    
076    /** The category provider */
077    protected CategoryProviderExtensionPoint _categoryProviderEP;
078    
079    /** The category helper */
080    protected CategoryHelper _categoryHelper;
081
082    /** The right manager */
083    protected RightManager _rightManager;
084
085    private WorkspaceModuleExtensionPoint _modulesEP;
086    
087    @Override
088    public void service(ServiceManager smanager) throws ServiceException
089    {
090        super.service(smanager);
091        _searchModuleEP = (SearchModuleExtensionPoint) smanager.lookup(SearchModuleExtensionPoint.ROLE);
092        _sourceResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
093        _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE);
094        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
095        _projectMemberManager = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE);
096        _categoryProviderEP = (CategoryProviderExtensionPoint) smanager.lookup(CategoryProviderExtensionPoint.ROLE);
097        _categoryHelper = (CategoryHelper) smanager.lookup(CategoryHelper.ROLE);
098        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
099        _modulesEP = (WorkspaceModuleExtensionPoint) smanager.lookup(WorkspaceModuleExtensionPoint.ROLE);
100    }
101    
102    public void generate() throws IOException, SAXException, ProcessingException
103    {
104        boolean withResults = parameters.getParameterAsBoolean("withResults", false);
105        
106        Request request = ObjectModelHelper.getRequest(objectModel);
107        String moduleId = request.getParameter("moduleId");
108        
109        contentHandler.startDocument();
110        XMLUtils.startElement(contentHandler, "search");
111        
112        saxFilters(request);
113        
114        if (moduleId != null)
115        {
116            // Load more results
117            SearchModule searchModule = _searchModuleEP.getExtension(moduleId);
118            int offset = parameters.getParameterAsInteger("offset", 0);
119            saxSearchModule(searchModule, withResults, offset);
120        }
121        else 
122        {
123            List<Project> availableProjects = getAvailableProjects();
124            saxProjects(availableProjects);
125            saxCategories(availableProjects);
126            saxSearchModules(availableProjects, withResults);
127        }
128        
129        XMLUtils.endElement(contentHandler, "search");
130        contentHandler.endDocument();
131    }
132    
133    
134
135    /**
136     * SAX the available search modules among available projects
137     * @param availableProjects The available projects
138     * @param withResults true to set results
139     * @throws SAXException if an error occurred while saxing
140     */
141    protected void saxSearchModules(List<Project> availableProjects, boolean withResults) throws SAXException
142    {
143        XMLUtils.startElement(contentHandler, "modules");
144        
145        List<SearchModule> searchModules = _searchModuleEP.getExtensionsIds()
146            .stream()
147            .map(_searchModuleEP::getExtension)
148            .filter(s -> isSearchModuleAvailable(s, availableProjects))
149            .sorted(Comparator.comparingInt(s -> s.getOrder()))
150            .toList();
151        
152        for (SearchModule searchModule : searchModules)
153        {
154            saxSearchModule(searchModule, withResults, 0);
155        }
156        
157        XMLUtils.endElement(contentHandler, "modules");
158    }
159    
160    /**
161     * Determines if search module is available for current user among available project
162     * @param searchModule the search modile
163     * @param availableProjects the available projects
164     * @return true if module is available
165     */
166    protected boolean isSearchModuleAvailable(SearchModule searchModule, List<Project> availableProjects)
167    {
168        String refModuleId = searchModule.getReferenceModuleId();
169        if (refModuleId == null)
170        {
171            return true;
172        }
173        
174        WorkspaceModule module = _modulesEP.getModule(refModuleId);
175        return availableProjects.stream()
176                .filter(project -> _projectManager.isModuleActivated(project, refModuleId))
177                .filter(project -> _rightManager.currentUserHasReadAccess(module.getModuleRoot(project, false)))
178                .findAny()
179                .isPresent();
180    }
181    
182    /**
183     * SAX the existing search module
184     * @param searchModule The search module
185     * @param withResults true to set results
186     * @param offset the offset search
187     * @throws SAXException if an error occurred while saxing
188     */
189    protected void saxSearchModule(SearchModule searchModule, boolean withResults, int offset) throws SAXException
190    {
191        AttributesImpl attrs = new AttributesImpl();
192        attrs.addCDATAAttribute("id", searchModule.getId());
193        attrs.addCDATAAttribute("url", searchModule.getSearchUrl());
194        attrs.addCDATAAttribute("order", String.valueOf(searchModule.getOrder()));
195        attrs.addCDATAAttribute("offset", String.valueOf(offset));
196        attrs.addCDATAAttribute("limit", String.valueOf(searchModule.getLimit()));
197        attrs.addCDATAAttribute("minLimit", String.valueOf(searchModule.getMinLimit()));
198        
199        XMLUtils.startElement(contentHandler, "module", attrs);
200        searchModule.getTitle().toSAX(contentHandler, "title");
201        
202        if (withResults)
203        {
204            saxSearchModuleResults(searchModule, offset);
205        }
206        XMLUtils.endElement(contentHandler, "module");
207    }
208    /**
209     * Sax the results of a search module
210     * @param searchModule the search module
211     * @param offset the offset search
212     */
213    protected void saxSearchModuleResults(SearchModule searchModule, int offset)
214    {
215        SitemapSource src = null; 
216        try
217        {
218            String uri = "cocoon://" + searchModule.getSearchUrl();
219            
220            Map<String, Object> params = new HashMap<>();
221            params.put("offset", offset);
222            params.put("limit", searchModule.getLimit());
223            params.put("minLimit", searchModule.getMinLimit());
224            
225            src = (SitemapSource) _sourceResolver.resolveURI(uri, null, params);
226            src.toSAX(new IgnoreRootHandler(contentHandler));
227        }
228        catch (SAXException | IOException e)
229        {
230            getLogger().error("The search failed for module '" + searchModule.getId() + "'", e);
231        }
232        finally
233        {
234            _sourceResolver.release(src);
235        }
236    }
237    
238    /**
239     * Get the available projects (the user's project)
240     * @return the available projects for search
241     */
242    protected List<Project> getAvailableProjects()
243    {
244        UserIdentity user = _currentUserProvider.getUser();
245
246        Function<Project, String> getProjectTitle = Project::getTitle;
247        Comparator<Project> projectTitleComparator = Comparator.comparing(getProjectTitle.andThen(StringUtils::stripAccents), String.CASE_INSENSITIVE_ORDER);
248        
249        return  _projectManager.getUserProjects(user)
250                .keySet()
251                .stream()
252                .sorted(projectTitleComparator)
253            .collect(Collectors.toList());
254    }
255    
256    /**
257     * SAX the active filters
258     * @param request the request
259     * @throws SAXException if an error occurred while saxing
260     */
261    protected void saxFilters(Request request) throws SAXException
262    {
263        XMLUtils.startElement(contentHandler, "filters");
264        
265        String textfield = request.getParameter("textfield");
266        if (StringUtils.isNotBlank(textfield))
267        {
268            XMLUtils.createElement(contentHandler, "textfield", textfield);
269        }
270        
271        String[] categories = request.getParameterValues("category");
272        if (categories != null)
273        {
274            for (String category : categories)
275            {
276                XMLUtils.createElement(contentHandler, "category", category);
277            }
278        }
279        
280        
281        String[] projects = request.getParameterValues("project");
282        if (projects != null)
283        {
284            for (String project : projects)
285            {
286                XMLUtils.createElement(contentHandler, "project", project);
287            }
288        }
289        
290        XMLUtils.endElement(contentHandler, "filters");
291    }
292    
293    
294    /**
295     * SAX the available projects
296     * @param projects the projects to sax
297     * @throws SAXException if an error occurred while saxing
298     */
299    protected void saxProjects(List<Project> projects) throws SAXException
300    {
301        XMLUtils.startElement(contentHandler, "projects");
302        for (Project project : projects)
303        {
304            AttributesImpl attrs = new AttributesImpl();
305            attrs.addCDATAAttribute("id", project.getId());
306            attrs.addCDATAAttribute("name", project.getName());
307            XMLUtils.createElement(contentHandler, "project", attrs, project.getTitle());
308        }
309        XMLUtils.endElement(contentHandler, "projects");
310    }
311    
312    /**
313     * SAX the categories
314     * @param projects the available projects
315     * @throws SAXException if an error occurred while saxing
316     */
317    protected void saxCategories(List<Project> projects) throws SAXException
318    {
319        // Get the root categories from the available projects
320        Set<Category> rootCategories = projects.stream()
321            .map(Project::getCategories)
322            .flatMap(Collection::stream)
323            .map(id -> _categoryProviderEP.getTag(id, null))
324            .filter(Objects::nonNull)
325            .map(c -> _getRootCategory(c))
326            .collect(Collectors.toSet());
327        
328        XMLUtils.startElement(contentHandler, "categories");
329        for (Category category : rootCategories)
330        {
331            AttributesImpl attrs = new AttributesImpl();
332            attrs.addCDATAAttribute("id", category.getId());
333            attrs.addCDATAAttribute("name", category.getName());
334            XMLUtils.startElement(contentHandler, "category", attrs);
335            
336            Map<String, String> colors = _categoryHelper.getCategoryColor(category);
337            XMLUtils.createElement(contentHandler, "color", colors.get("main"));
338            category.getTitle().toSAX(contentHandler, "title");
339            XMLUtils.endElement(contentHandler, "category");
340        }
341        XMLUtils.endElement(contentHandler, "categories");
342    }
343    
344    private Category _getRootCategory(Category category)
345    {
346        Category parent = category;
347        while (parent.getParent() != null)
348        {
349            parent = parent.getParent();
350        }
351        return parent;
352    }
353    
354}