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