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                .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}