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