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