001/*
002 *  Copyright 2021 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.suggestions;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Comparator;
023import java.util.List;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.generation.ServiceableGenerator;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.collections.CollectionUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.xml.sax.SAXException;
037
038import org.ametys.core.user.CurrentUserProvider;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
041import org.ametys.plugins.workspaces.members.ProjectMemberManager;
042import org.ametys.plugins.workspaces.project.ProjectManager;
043import org.ametys.plugins.workspaces.project.ProjectsCatalogueManager;
044import org.ametys.plugins.workspaces.project.helper.ProjectXsltHelper;
045import org.ametys.plugins.workspaces.project.objects.Project;
046import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
047import org.ametys.web.WebConstants;
048import org.ametys.web.repository.page.ZoneItem;
049
050/**
051 * Generator for suggested projects. The suggestion projects are:<br/>
052 * - those of same categories OR with common keywords with the current project<br/>
053 * - those the current user is not already a member<br/>
054 * - sorted by common keywords
055 */
056public class ProjectsSuggestionsGenerator extends ServiceableGenerator
057{
058    /** The project manager */
059    protected ProjectManager _projectManager;
060    /** The project memebers manager */
061    protected ProjectMemberManager _projectMembers;
062    /** The project catalog manager */
063    protected ProjectsCatalogueManager _projectCatalogManager;
064    /** The current user provider */
065    protected CurrentUserProvider _currentUserProvider;
066
067    @Override
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        super.service(smanager);
071        _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE);
072        _projectMembers = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE);
073        _projectCatalogManager = (ProjectsCatalogueManager) smanager.lookup(ProjectsCatalogueManager.ROLE);
074        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
075    }
076    
077    public void generate() throws IOException, SAXException, ProcessingException
078    {
079        contentHandler.startDocument();
080        XMLUtils.startElement(contentHandler, "suggestions");
081        
082        String projectName = ProjectXsltHelper.project();
083        
084        if (StringUtils.isEmpty(projectName))
085        {
086            getLogger().warn("There is no current project to get the project suggestions");
087        }
088        else
089        {
090            Request request = ObjectModelHelper.getRequest(objectModel);
091            
092            ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
093            Long nbMax = 0L;
094            
095            if (zoneItem != null)
096            {
097                ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
098                if (serviceParameters != null && serviceParameters.hasValue("maxlength"))
099                {
100                    nbMax = zoneItem.getServiceParameters().getValue("maxlength");
101                }
102            }
103            
104            List<Project> suggestedProjects = getSuggestedProjects(projectName, _currentUserProvider.getUser(), nbMax.equals(0L) ? Integer.MAX_VALUE : nbMax.intValue());
105            
106            
107            for (Project suggestedProject : suggestedProjects)
108            {
109                _projectCatalogManager.saxProject(contentHandler, suggestedProject);
110            }
111        }
112        
113        XMLUtils.endElement(contentHandler, "suggestions");
114        contentHandler.endDocument();
115    }
116    
117    /**
118     * Get the suggested projects for a given project and a user 
119     * @param projectName the project name
120     * @param user the user
121     * @param max the max number of suggestions
122     * @return the related projects
123     */
124    protected List<Project> getSuggestedProjects(String projectName, UserIdentity user, int max)
125    {
126        Project project = _projectManager.getProject(projectName);
127        if (project != null)
128        {
129            Set<String> projectCategories = project.getCategories();
130            List<String> projectKeywords = Arrays.asList(project.getKeywords());
131            
132            // Get the projects with common categories or common keywords
133            List<Project> relatedProjects = _projectManager.getProjects(new ArrayList<>(projectCategories), projectKeywords, true);
134            
135            List<Project> projects = relatedProjects.stream()
136                .filter(p -> !p.getName().equals(projectName)) // exclude current project
137                .filter(p -> p.getInscriptionStatus() != InscriptionStatus.PRIVATE) // public or moderate projects only
138                .filter(p -> _projectMembers.getProjectMember(p, user) == null) // exclude user's project
139                .filter(p -> _projectManager.isUserInProjectPopulations(p, user)) // exclude project without current user population
140                .collect(Collectors.toList());
141            
142            // Sort by common categories then keywords with current project
143            projects.sort(new ProjectComparator(projectCategories, projectKeywords));
144            
145            return projects.subList(0, Integer.min(projects.size(), max));
146        }
147        else
148        {
149            getLogger().warn("Unknown project with name '" + projectName + "'");
150            return List.of();
151        }
152    }
153    
154    class ProjectComparator implements Comparator<Project>
155    {
156        private Collection<String> _baseKeywords;
157        private Collection<String> _baseCategories;
158        
159        public ProjectComparator(Collection<String> baseCategories, Collection<String> baseKeywords)
160        {
161            _baseCategories = baseCategories;
162            _baseKeywords = baseKeywords;
163        }
164        
165        public int compare(Project p1, Project p2)
166        {
167            int score1 = countCommonCategories(p1) * 2 + countCommonKeywords(p1);
168            int score2 = countCommonCategories(p2) * 2 + countCommonKeywords(p2);
169            return score2 - score1;
170        }
171        
172        int countCommonCategories(Project project)
173        {
174            Set<String> projectCategories = project.getCategories();
175            
176            if (_baseCategories.isEmpty() || projectCategories.isEmpty())
177            {
178                // no common categories
179                return 0;
180            }
181            return CollectionUtils.intersection(_baseCategories, projectCategories).size();
182        }
183        
184        int countCommonKeywords(Project project)
185        {
186            List<String> projectKeywords = Arrays.asList(project.getKeywords());
187            
188            if (_baseKeywords.isEmpty() || projectKeywords.isEmpty())
189            {
190                // no common keywords
191                return 0;
192            }
193            return CollectionUtils.intersection(_baseKeywords, projectKeywords).size();
194        }
195        
196    }
197
198}