001/* 002 * Copyright 2020 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.search; 017 018import java.io.IOException; 019import java.util.Collection; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Set; 026import java.util.function.Function; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.ProcessingException; 032import org.apache.cocoon.components.source.impl.SitemapSource; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.generation.ServiceableGenerator; 036import org.apache.cocoon.xml.AttributesImpl; 037import org.apache.cocoon.xml.XMLUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.excalibur.source.SourceResolver; 040import org.xml.sax.SAXException; 041 042import org.ametys.core.right.RightManager; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.util.IgnoreRootHandler; 046import org.ametys.plugins.workspaces.categories.Category; 047import org.ametys.plugins.workspaces.categories.CategoryHelper; 048import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint; 049import org.ametys.plugins.workspaces.members.ProjectMemberManager; 050import org.ametys.plugins.workspaces.project.ProjectManager; 051import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 052import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 053import org.ametys.plugins.workspaces.project.objects.Project; 054 055/** 056 * Generator for modular search service 057 * 058 */ 059public class ModularSearchGenerator extends ServiceableGenerator 060{ 061 /** Search Module Extension Point */ 062 protected SearchModuleExtensionPoint _searchModuleEP; 063 064 /** Source Resolver */ 065 protected SourceResolver _sourceResolver; 066 067 /** The project manager */ 068 protected ProjectManager _projectManager; 069 070 /** The current user provider */ 071 protected CurrentUserProvider _currentUserProvider; 072 073 /** The project member manager */ 074 protected ProjectMemberManager _projectMemberManager; 075 076 /** The category provider */ 077 protected CategoryProviderExtensionPoint _categoryProviderEP; 078 079 /** The category helper */ 080 protected CategoryHelper _categoryHelper; 081 082 /** The right manager */ 083 protected RightManager _rightManager; 084 085 private WorkspaceModuleExtensionPoint _modulesEP; 086 087 @Override 088 public void service(ServiceManager smanager) throws ServiceException 089 { 090 super.service(smanager); 091 _searchModuleEP = (SearchModuleExtensionPoint) smanager.lookup(SearchModuleExtensionPoint.ROLE); 092 _sourceResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE); 093 _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE); 094 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 095 _projectMemberManager = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE); 096 _categoryProviderEP = (CategoryProviderExtensionPoint) smanager.lookup(CategoryProviderExtensionPoint.ROLE); 097 _categoryHelper = (CategoryHelper) smanager.lookup(CategoryHelper.ROLE); 098 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 099 _modulesEP = (WorkspaceModuleExtensionPoint) smanager.lookup(WorkspaceModuleExtensionPoint.ROLE); 100 } 101 102 public void generate() throws IOException, SAXException, ProcessingException 103 { 104 boolean withResults = parameters.getParameterAsBoolean("withResults", false); 105 106 Request request = ObjectModelHelper.getRequest(objectModel); 107 String moduleId = request.getParameter("moduleId"); 108 109 contentHandler.startDocument(); 110 XMLUtils.startElement(contentHandler, "search"); 111 112 saxFilters(request); 113 114 if (moduleId != null) 115 { 116 // Load more results 117 SearchModule searchModule = _searchModuleEP.getExtension(moduleId); 118 int offset = parameters.getParameterAsInteger("offset", 0); 119 saxSearchModule(searchModule, withResults, offset); 120 } 121 else 122 { 123 List<Project> availableProjects = getAvailableProjects(); 124 saxProjects(availableProjects); 125 saxCategories(availableProjects); 126 saxSearchModules(availableProjects, withResults); 127 } 128 129 XMLUtils.endElement(contentHandler, "search"); 130 contentHandler.endDocument(); 131 } 132 133 134 135 /** 136 * SAX the available search modules among available projects 137 * @param availableProjects The available projects 138 * @param withResults true to set results 139 * @throws SAXException if an error occurred while saxing 140 */ 141 protected void saxSearchModules(List<Project> availableProjects, boolean withResults) throws SAXException 142 { 143 XMLUtils.startElement(contentHandler, "modules"); 144 145 List<SearchModule> searchModules = _searchModuleEP.getExtensionsIds() 146 .stream() 147 .map(_searchModuleEP::getExtension) 148 .filter(s -> isSearchModuleAvailable(s, availableProjects)) 149 .sorted(Comparator.comparingInt(s -> s.getOrder())) 150 .toList(); 151 152 for (SearchModule searchModule : searchModules) 153 { 154 saxSearchModule(searchModule, withResults, 0); 155 } 156 157 XMLUtils.endElement(contentHandler, "modules"); 158 } 159 160 /** 161 * Determines if search module is available for current user among available project 162 * @param searchModule the search modile 163 * @param availableProjects the available projects 164 * @return true if module is available 165 */ 166 protected boolean isSearchModuleAvailable(SearchModule searchModule, List<Project> availableProjects) 167 { 168 String refModuleId = searchModule.getReferenceModuleId(); 169 if (refModuleId == null) 170 { 171 return true; 172 } 173 174 WorkspaceModule module = _modulesEP.getModule(refModuleId); 175 return availableProjects.stream() 176 .filter(project -> _projectManager.isModuleActivated(project, refModuleId)) 177 .filter(project -> _rightManager.currentUserHasReadAccess(module.getModuleRoot(project, false))) 178 .findAny() 179 .isPresent(); 180 } 181 182 /** 183 * SAX the existing search module 184 * @param searchModule The search module 185 * @param withResults true to set results 186 * @param offset the offset search 187 * @throws SAXException if an error occurred while saxing 188 */ 189 protected void saxSearchModule(SearchModule searchModule, boolean withResults, int offset) throws SAXException 190 { 191 AttributesImpl attrs = new AttributesImpl(); 192 attrs.addCDATAAttribute("id", searchModule.getId()); 193 attrs.addCDATAAttribute("url", searchModule.getSearchUrl()); 194 attrs.addCDATAAttribute("order", String.valueOf(searchModule.getOrder())); 195 attrs.addCDATAAttribute("offset", String.valueOf(offset)); 196 attrs.addCDATAAttribute("limit", String.valueOf(searchModule.getLimit())); 197 attrs.addCDATAAttribute("minLimit", String.valueOf(searchModule.getMinLimit())); 198 199 XMLUtils.startElement(contentHandler, "module", attrs); 200 searchModule.getTitle().toSAX(contentHandler, "title"); 201 202 if (withResults) 203 { 204 saxSearchModuleResults(searchModule, offset); 205 } 206 XMLUtils.endElement(contentHandler, "module"); 207 } 208 /** 209 * Sax the results of a search module 210 * @param searchModule the search module 211 * @param offset the offset search 212 */ 213 protected void saxSearchModuleResults(SearchModule searchModule, int offset) 214 { 215 SitemapSource src = null; 216 try 217 { 218 String uri = "cocoon://" + searchModule.getSearchUrl(); 219 220 Map<String, Object> params = new HashMap<>(); 221 params.put("offset", offset); 222 params.put("limit", searchModule.getLimit()); 223 params.put("minLimit", searchModule.getMinLimit()); 224 225 src = (SitemapSource) _sourceResolver.resolveURI(uri, null, params); 226 src.toSAX(new IgnoreRootHandler(contentHandler)); 227 } 228 catch (SAXException | IOException e) 229 { 230 getLogger().error("The search failed for module '" + searchModule.getId() + "'", e); 231 } 232 finally 233 { 234 _sourceResolver.release(src); 235 } 236 } 237 238 /** 239 * Get the available projects (the user's project) 240 * @return the available projects for search 241 */ 242 protected List<Project> getAvailableProjects() 243 { 244 UserIdentity user = _currentUserProvider.getUser(); 245 246 Function<Project, String> getProjectTitle = Project::getTitle; 247 Comparator<Project> projectTitleComparator = Comparator.comparing(getProjectTitle.andThen(StringUtils::stripAccents), String.CASE_INSENSITIVE_ORDER); 248 249 return _projectManager.getUserProjects(user) 250 .keySet() 251 .stream() 252 .sorted(projectTitleComparator) 253 .collect(Collectors.toList()); 254 } 255 256 /** 257 * SAX the active filters 258 * @param request the request 259 * @throws SAXException if an error occurred while saxing 260 */ 261 protected void saxFilters(Request request) throws SAXException 262 { 263 XMLUtils.startElement(contentHandler, "filters"); 264 265 String textfield = request.getParameter("textfield"); 266 if (StringUtils.isNotBlank(textfield)) 267 { 268 XMLUtils.createElement(contentHandler, "textfield", textfield); 269 } 270 271 String[] categories = request.getParameterValues("category"); 272 if (categories != null) 273 { 274 for (String category : categories) 275 { 276 XMLUtils.createElement(contentHandler, "category", category); 277 } 278 } 279 280 281 String[] projects = request.getParameterValues("project"); 282 if (projects != null) 283 { 284 for (String project : projects) 285 { 286 XMLUtils.createElement(contentHandler, "project", project); 287 } 288 } 289 290 XMLUtils.endElement(contentHandler, "filters"); 291 } 292 293 294 /** 295 * SAX the available projects 296 * @param projects the projects to sax 297 * @throws SAXException if an error occurred while saxing 298 */ 299 protected void saxProjects(List<Project> projects) throws SAXException 300 { 301 XMLUtils.startElement(contentHandler, "projects"); 302 for (Project project : projects) 303 { 304 AttributesImpl attrs = new AttributesImpl(); 305 attrs.addCDATAAttribute("id", project.getId()); 306 attrs.addCDATAAttribute("name", project.getName()); 307 XMLUtils.createElement(contentHandler, "project", attrs, project.getTitle()); 308 } 309 XMLUtils.endElement(contentHandler, "projects"); 310 } 311 312 /** 313 * SAX the categories 314 * @param projects the available projects 315 * @throws SAXException if an error occurred while saxing 316 */ 317 protected void saxCategories(List<Project> projects) throws SAXException 318 { 319 // Get the root categories from the available projects 320 Set<Category> rootCategories = projects.stream() 321 .map(Project::getCategories) 322 .flatMap(Collection::stream) 323 .map(id -> _categoryProviderEP.getTag(id, null)) 324 .filter(Objects::nonNull) 325 .map(c -> _getRootCategory(c)) 326 .collect(Collectors.toSet()); 327 328 XMLUtils.startElement(contentHandler, "categories"); 329 for (Category category : rootCategories) 330 { 331 AttributesImpl attrs = new AttributesImpl(); 332 attrs.addCDATAAttribute("id", category.getId()); 333 attrs.addCDATAAttribute("name", category.getName()); 334 XMLUtils.startElement(contentHandler, "category", attrs); 335 336 Map<String, String> colors = _categoryHelper.getCategoryColor(category); 337 XMLUtils.createElement(contentHandler, "color", colors.get("main")); 338 category.getTitle().toSAX(contentHandler, "title"); 339 XMLUtils.endElement(contentHandler, "category"); 340 } 341 XMLUtils.endElement(contentHandler, "categories"); 342 } 343 344 private Category _getRootCategory(Category category) 345 { 346 Category parent = category; 347 while (parent.getParent() != null) 348 { 349 parent = parent.getParent(); 350 } 351 return parent; 352 } 353 354}