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.module;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.ProcessingException;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.generation.ServiceableGenerator;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.lang3.StringUtils;
035import org.xml.sax.SAXException;
036
037import org.ametys.core.right.RightManager;
038import org.ametys.core.user.CurrentUserProvider;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.util.LambdaUtils;
041import org.ametys.plugins.core.user.UserHelper;
042import org.ametys.plugins.repository.AmetysObject;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.workspaces.categories.Category;
045import org.ametys.plugins.workspaces.categories.CategoryHelper;
046import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint;
047import org.ametys.plugins.workspaces.members.ProjectMemberManager;
048import org.ametys.plugins.workspaces.project.ProjectManager;
049import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
050import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
051import org.ametys.plugins.workspaces.project.objects.Project;
052import org.ametys.web.WebConstants;
053import org.ametys.web.WebHelper;
054import org.ametys.web.repository.site.SiteManager;
055
056/**
057 * Abstract generator for search modules
058 *
059 */
060public abstract class AbstractSearchModuleGenerator extends ServiceableGenerator
061{
062    /** The ametys object resolver */
063    protected AmetysObjectResolver _resolver;
064    /** The project manager */
065    protected ProjectManager _projectManager;
066    /** The current user provider */
067    protected CurrentUserProvider _currentUserProvider;
068    /** The project member manager */
069    protected ProjectMemberManager _projectMembers;
070    /** CategoryProviderExtensionPoint */
071    protected CategoryProviderExtensionPoint _categoryProviderEP;
072    /** The category helper */
073    protected CategoryHelper _categoryHelper;
074    /** The user helper */
075    protected UserHelper _userHelper;
076    /** The site manager */
077    protected SiteManager _siteManager;
078    /** WorkspaceModuleExtensionPoint */
079    protected WorkspaceModuleExtensionPoint _workspaceModuleEP;
080    /** Right manager */
081    protected RightManager _rightManager;
082    
083    @Override
084    public void service(ServiceManager smanager) throws ServiceException
085    {
086        super.service(smanager);
087        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
088        _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE);
089        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
090        _projectMembers = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE);
091        _categoryProviderEP = (CategoryProviderExtensionPoint) smanager.lookup(CategoryProviderExtensionPoint.ROLE);
092        _categoryHelper = (CategoryHelper) smanager.lookup(CategoryHelper.ROLE);
093        _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE);
094        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
095        _workspaceModuleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
096        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
097    }
098    
099    public void generate() throws IOException, SAXException, ProcessingException
100    {
101        Request request = ObjectModelHelper.getRequest(objectModel);
102        
103        String siteName = WebHelper.getSiteName(request);
104        String lang = request.getParameter("lang");
105        String textfield = request.getParameter("textfield");
106        
107        request.setAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME, lang);
108        request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, _siteManager.getSite(siteName).getSkinId());
109        
110        int offset = parameters.getParameterAsInteger("offset", 0);
111        int limit = parameters.getParameterAsInteger("limit", 10);
112        int minLimit = parameters.getParameterAsInteger("minLimit", 10);
113        
114        contentHandler.startDocument();
115        
116        saxHits(siteName, lang, textfield, request, offset, limit, minLimit);
117        
118        contentHandler.endDocument();
119    }
120    
121    /**
122     * Sax the results after search
123     * @param results list of results
124     * @param lang the current language
125     * @param offset the start of search
126     * @param limit the max number of results
127     * @param minLimit the min number of results
128     * @param totalCount total count available
129     * @throws SAXException if an error occurred while saxing
130     */
131    protected void saxHits(List<? extends AmetysObject> results, String lang, int offset, int limit, int minLimit, long totalCount) throws SAXException
132    {
133        AttributesImpl attrs = new AttributesImpl();
134        attrs.addCDATAAttribute("count", String.valueOf(results.size()));
135        attrs.addCDATAAttribute("total", String.valueOf(totalCount));
136        attrs.addCDATAAttribute("offset", String.valueOf(offset));
137        attrs.addCDATAAttribute("limit", String.valueOf(limit));
138        attrs.addCDATAAttribute("minLimit", String.valueOf(minLimit));
139        
140        XMLUtils.startElement(contentHandler, "hits", attrs);
141        
142        for (AmetysObject result : results)
143        {
144            try
145            {
146                saxHit(result, lang);
147            }
148            catch (Exception e)
149            {
150                getLogger().error("Unable to sax result for object of id '" + result.getId() + "'", e);
151            }
152        }
153        
154        XMLUtils.endElement(contentHandler, "hits");
155    }
156    
157    /**
158     * Get the project holding the object.
159     * Be careful, use only for objects that belongs to a project (thread, task, event, ...)
160     * @param ao the Ametys object
161     * @return the project or null if nor found
162     */
163    protected Project getProject(AmetysObject ao)
164    {
165        AmetysObject parent = ao.getParent();
166        
167        while (parent != null && !(parent instanceof Project))
168        {
169            parent = parent.getParent();
170        }
171        
172        return parent != null ? (Project) parent : null;
173    }
174    
175    /**
176     * SAX a project
177     * @param project the project
178     * @throws SAXException if an error occurred while saxing
179     */
180    protected void saxProject(Project project) throws SAXException
181    {
182        if (project != null)
183        {
184            AttributesImpl attrs = new AttributesImpl();
185            attrs.addCDATAAttribute("name", project.getName());
186            XMLUtils.startElement(contentHandler, "project", attrs);
187            XMLUtils.createElement(contentHandler, "title", project.getTitle());
188            Category category = project.getCategories()
189                .stream()
190                .findFirst()
191                .map(c -> _categoryProviderEP.getTag(c, null))
192                .orElse(null);
193            saxCategory(category);
194            XMLUtils.endElement(contentHandler, "project");
195            
196        }
197    }
198    
199    /**
200     * SAX category
201     * @param category the category
202     * @throws SAXException if an error occured while saxing
203     */
204    protected void saxCategory(Category category) throws SAXException
205    {
206        if (category != null)
207        {
208            AttributesImpl attrs = new AttributesImpl();
209            attrs.addCDATAAttribute("name", category.getName());
210            XMLUtils.startElement(contentHandler, "category", attrs);
211            category.getTitle().toSAX(contentHandler, "title");
212            saxCategoryColor(category);
213            XMLUtils.endElement(contentHandler, "category");
214        }
215    }
216    
217    /**
218     * SAX the category color
219     * @param category the category
220     * @throws SAXException if an error occured while saxing
221     */
222    protected void saxCategoryColor(Category category) throws SAXException
223    {
224        Map<String, String> colors = _categoryHelper.getCategoryColor(category);
225        
226        XMLUtils.startElement(contentHandler, "color");
227        colors.entrySet().stream()
228            .forEach(LambdaUtils.wrapConsumer(entry -> XMLUtils.createElement(contentHandler, entry.getKey(), entry.getValue())));
229        XMLUtils.endElement(contentHandler, "color");
230    }
231    
232    /**
233     * Get the project names targeted by the search
234     * @param request the request
235     * @param userOnly true to get user's projects only
236     * @return the project names
237     */
238    public List<Project> getProjects(Request request, boolean userOnly)
239    {
240        List<String> projectNames = Arrays.asList(request.getParameter("project").split(","))
241                .stream()
242                .filter(StringUtils::isNotEmpty)
243                .collect(Collectors.toList());
244        
245        List<String> filteredCategories = getCategories(request);
246        
247        if (projectNames.size() > 0)
248        {
249            return projectNames
250                .stream()
251                .map(p -> _projectManager.getProject(p))
252                .filter(p -> !Collections.disjoint(p.getCategories(), filteredCategories))
253                .collect(Collectors.toList());
254        }
255        else if (userOnly)
256        {
257            // Get user projects
258            UserIdentity user = _currentUserProvider.getUser();
259            
260            return  _projectManager.getUserProjects(user, filteredCategories)
261                    .keySet()
262                    .stream()
263                    .collect(Collectors.toList());
264        }
265        else
266        {
267            // All projects
268            return  _projectManager.getProjects(filteredCategories);
269        }
270    }
271    
272    /**
273     * Filter a list of projects to return only those where the module is available
274     * @param projects list of projects
275     * @param moduleId id of the module to search for read access
276     * @return a filtered list of projects
277     */
278    protected List<Project> filterProjectsForModule(List<Project> projects, String moduleId)
279    {
280        WorkspaceModule module = _workspaceModuleEP.getModule(moduleId);
281        
282        if (module == null || projects == null || projects.isEmpty())
283        {
284            return Collections.EMPTY_LIST;
285        }
286        
287        List<Project> filteredProjects = projects.stream()
288                .filter(project -> _projectManager.isModuleActivated(project, module.getId()))
289                .filter(project -> _rightManager.currentUserHasReadAccess(module.getModuleRoot(project, false)))
290                .collect(Collectors.toList());
291        
292        return filteredProjects;
293    }
294    
295    /**
296     * Get the category names targeted by the search
297     * @param request the request
298     * @return the category names
299     */
300    protected List<String> getCategories(Request request)
301    {
302        List<String> categorieNames = Arrays.asList(request.getParameter("category").split(","))
303                .stream()
304                .filter(StringUtils::isNotEmpty)
305                .collect(Collectors.toList());
306        
307        if (categorieNames.isEmpty())
308        {
309            // Get all leaf categories
310            return _categoryHelper.getLeafCategories()
311                .stream()
312                .map(Category::getName)
313                .collect(Collectors.toList());
314        }
315        else
316        {
317            List<String> filteredCategories = new ArrayList<>();
318            for (String categoryName : categorieNames)
319            {
320                Category category = _categoryProviderEP.getTag(categoryName, Collections.EMPTY_MAP);
321                filteredCategories.addAll(_categoryHelper.getLeafCategories(category)
322                        .stream()
323                        .map(Category::getName)
324                        .collect(Collectors.toList()));
325            }
326            
327            return filteredCategories;
328        }
329    }
330    
331    /**
332     * Sax the results
333     * @param siteName the current site name
334     * @param lang the current language
335     * @param textfield the search inputs
336     * @param request the request
337     * @param offset the start of search
338     * @param limit the max number of results
339     * @param minLimit the min number of results
340     * @throws SAXException if an error occurred while saxing
341     * @throws ProcessingException if the search failed
342     */
343    protected abstract void saxHits(String siteName, String lang, String textfield, Request request, int offset, int limit, int minLimit) throws SAXException, ProcessingException;
344
345    /**
346     * Sax the content hit
347     * @param object the AmetysObject
348     * @param lang the language
349     * @throws Exception if an error occurred while saxing result
350     */
351    protected abstract void saxHit(AmetysObject object, String lang) throws Exception;
352    
353    /**
354     * SAX a user identity
355     * @param userIdentity the user identity
356     * @param tagName the tag name
357     * @throws SAXException if an error occurred while saxing
358     */
359    protected void saxUser(UserIdentity userIdentity, String tagName) throws SAXException
360    {
361        if (userIdentity != null)
362        {
363            _userHelper.saxUserIdentity(userIdentity, contentHandler, tagName);
364        }
365    }
366    
367}