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}