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}