001/* 002 * Copyright 2017 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.project; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Set; 031import java.util.function.Predicate; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.context.Context; 037import org.apache.avalon.framework.context.ContextException; 038import org.apache.avalon.framework.context.Contextualizable; 039import org.apache.avalon.framework.logger.AbstractLogEnabled; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043import org.apache.cocoon.servlet.multipart.Part; 044import org.apache.cocoon.xml.AttributesImpl; 045import org.apache.cocoon.xml.XMLUtils; 046import org.apache.commons.collections.CollectionUtils; 047import org.apache.commons.lang.StringUtils; 048import org.apache.commons.lang3.tuple.Pair; 049import org.apache.excalibur.source.Source; 050import org.apache.excalibur.source.SourceResolver; 051import org.apache.tika.io.FilenameUtils; 052import org.xml.sax.ContentHandler; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.languages.LanguagesManager; 056import org.ametys.cms.transformation.xslt.ResolveURIComponent; 057import org.ametys.core.group.GroupDirectoryContextHelper; 058import org.ametys.core.observation.Event; 059import org.ametys.core.observation.ObservationManager; 060import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint; 061import org.ametys.core.right.RightManager; 062import org.ametys.core.right.RightManager.RightResult; 063import org.ametys.core.ui.Callable; 064import org.ametys.core.user.CurrentUserProvider; 065import org.ametys.core.user.User; 066import org.ametys.core.user.UserIdentity; 067import org.ametys.core.user.UserManager; 068import org.ametys.core.user.population.PopulationContextHelper; 069import org.ametys.core.util.I18nUtils; 070import org.ametys.core.util.URIUtils; 071import org.ametys.core.util.mail.SendMailHelper; 072import org.ametys.plugins.core.user.UserHelper; 073import org.ametys.plugins.repository.AmetysObjectResolver; 074import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 075import org.ametys.plugins.repository.jcr.NameHelper; 076import org.ametys.plugins.workspaces.about.AboutWorkspaceModule; 077import org.ametys.plugins.workspaces.alert.AlertWorkspaceModule; 078import org.ametys.plugins.workspaces.categories.Category; 079import org.ametys.plugins.workspaces.categories.CategoryHelper; 080import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint; 081import org.ametys.plugins.workspaces.keywords.KeywordProviderExtensionPoint; 082import org.ametys.plugins.workspaces.keywords.KeywordsDAO; 083import org.ametys.plugins.workspaces.members.MembersWorkspaceModule; 084import org.ametys.plugins.workspaces.members.ProjectMemberManager; 085import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember; 086import org.ametys.plugins.workspaces.news.NewsWorkspaceModule; 087import org.ametys.plugins.workspaces.project.favorites.FavoritesHelper; 088import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 089import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper; 090import org.ametys.plugins.workspaces.project.objects.Project; 091import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus; 092import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 093import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 094import org.ametys.runtime.config.Config; 095import org.ametys.runtime.i18n.I18nizableText; 096import org.ametys.runtime.i18n.I18nizableTextParameter; 097import org.ametys.web.ObservationConstants; 098import org.ametys.web.repository.page.Page; 099import org.ametys.web.repository.page.SitemapElement; 100import org.ametys.web.repository.page.ZoneItem; 101import org.ametys.web.repository.site.Site; 102import org.ametys.web.repository.site.SiteDAO; 103import org.ametys.web.site.SiteConfigurationManager; 104 105import jakarta.mail.MessagingException; 106 107/** 108 * Manager for the Projects Catalogue service 109 */ 110public class ProjectsCatalogueManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 111{ 112 /** Avalon Role */ 113 public static final String ROLE = ProjectsCatalogueManager.class.getName(); 114 115 /** The identifier of modules that are always active */ 116 public static final Set<String> DEFAULT_MODULES = Set.of(MembersWorkspaceModule.MEMBERS_MODULE_ID, 117 AboutWorkspaceModule.ABOUT_MODULE_ID, 118 NewsWorkspaceModule.NEWS_MODULE_ID, 119 AlertWorkspaceModule.ALERT_MODULE_ID); 120 121 /** List of allowed field received from the front */ 122 private static final String[] __ALLOWED_FORM_DATA = {"description", "emailList", "inscriptionStatus", "defaultProfile", "tags", "categoryTags", "keywords"}; 123 124 /** Ametys Object Resolver */ 125 protected AmetysObjectResolver _resolver; 126 /** Current user provider */ 127 protected CurrentUserProvider _currentUserProvider; 128 /** The project members' manager */ 129 protected ProjectMemberManager _projectMemberManager; 130 131 /** The right manager */ 132 protected RightManager _rightManager; 133 /** The project manager */ 134 protected ProjectManager _projectManager; 135 /** Helper for project's rights */ 136 protected ProjectRightHelper _projectRightHelper; 137 /** The language manager */ 138 protected LanguagesManager _languagesManager; 139 /** The source resolver */ 140 protected SourceResolver _sourceResolver; 141 /** The site dao */ 142 protected SiteDAO _siteDAO; 143 /** The site's configuration manager */ 144 protected SiteConfigurationManager _siteConfigurationManager; 145 /** Helper for user population */ 146 protected PopulationContextHelper _populationContextHelper; 147 /** The extension point for workspace's modules */ 148 protected WorkspaceModuleExtensionPoint _moduleManagerEP; 149 /** The extension point for profiles' storage */ 150 protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint; 151 /** The user manager */ 152 protected UserManager _userManager; 153 /** Utils for i18n */ 154 protected I18nUtils _i18nUtils; 155 /** The observation manager */ 156 protected ObservationManager _observationManager; 157 /** Helper for group directory's context */ 158 protected GroupDirectoryContextHelper _groupDirectoryContextHelper; 159 /** The extension point for project's tags */ 160 protected ProjectTagProviderExtensionPoint _projectTagProviderEP; 161 /** The extension point for project's categories */ 162 protected CategoryProviderExtensionPoint _categoryProviderEP; 163 /** The extension point for project's keywords */ 164 protected KeywordProviderExtensionPoint _keywordProviderEP; 165 /** To handle favorites */ 166 protected FavoritesHelper _favoritesHelper; 167 /** The preference helper for notifications */ 168 protected NotificationPreferencesHelper _notificationPreferenceHelper; 169 170 /** The avalon context */ 171 protected Context _context; 172 173 private CategoryHelper _categoryHelper; 174 175 private ProjectMemberManager _projectMembers; 176 177 private UserHelper _userHelper; 178 179 private KeywordsDAO _keywordsDAO; 180 181 182 public void service(ServiceManager manager) throws ServiceException 183 { 184 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 185 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 186 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 187 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 188 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 189 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 190 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 191 _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 192 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 193 _siteConfigurationManager = (SiteConfigurationManager) manager.lookup(SiteConfigurationManager.ROLE); 194 _siteDAO = (SiteDAO) manager.lookup(SiteDAO.ROLE); 195 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 196 _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 197 _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE); 198 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 199 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 200 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 201 _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE); 202 _projectTagProviderEP = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 203 _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE); 204 _keywordProviderEP = (KeywordProviderExtensionPoint) manager.lookup(KeywordProviderExtensionPoint.ROLE); 205 _categoryHelper = (CategoryHelper) manager.lookup(CategoryHelper.ROLE); 206 _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 207 _keywordsDAO = (KeywordsDAO) manager.lookup(KeywordsDAO.ROLE); 208 _favoritesHelper = (FavoritesHelper) manager.lookup(FavoritesHelper.ROLE); 209 _notificationPreferenceHelper = (NotificationPreferencesHelper) manager.lookup(NotificationPreferencesHelper.ROLE); 210 } 211 212 public void contextualize(Context context) throws ContextException 213 { 214 _context = context; 215 } 216 217 private Pair<List<String>, List<Map<String, Object>>> _createMissingKeywords(List<Object> keywords) throws IllegalAccessException 218 { 219 List<Map<String, Object>> newKeywordsInfo = Collections.emptyList(); 220 String[] keywordsToCreate = keywords.stream().filter(t -> t instanceof Map).map(t -> (String) ((Map) t).get("text")).toArray(String[]::new); 221 if (keywordsToCreate.length > 0) 222 { 223 // check right 224 if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_HANDLE_PROJECTKEYWORDS, "/cms") != RightResult.RIGHT_ALLOW) 225 { 226 throw new IllegalAccessException("User " + _currentUserProvider.getUser() + " tried to create a project tag without the convinient rights"); 227 } 228 229 newKeywordsInfo = _keywordsDAO.addTags(keywordsToCreate); 230 } 231 232 Iterator<Map<String, Object>> newKeywordsInfoIterator = newKeywordsInfo.iterator(); 233 List<String> keywordsToSet = keywords.stream().map(t -> t instanceof String ? (String) t : (String) newKeywordsInfoIterator.next().get("name")).collect(Collectors.toList()); 234 return Pair.of(keywordsToSet, newKeywordsInfo); 235 } 236 237 /** 238 * Create a project 239 * @param zoneItemId The id of the zoneitem holding the catalog service 240 * @param title The title 241 * @param description The description (can be empty) 242 * @param illustration The illustration (can be a File or a local path) 243 * @param category The category 244 * @param keywords The project keywords 245 * @param visibility The visibility 246 * @param defaultProfile For public projects, profile for self registered users 247 * @param language The language code 248 * @param managers The managers url 249 * @param modules The selected modules 250 * @return Information about the new project 251 * @throws IllegalAccessException If user has no right to create tags and ask to 252 */ 253 @Callable 254 public Map<String, Object> createProject(String zoneItemId, String title, String description, Object illustration, String category, List<Object> keywords, Integer visibility, String defaultProfile, String language, List<String> managers, List<String> modules) throws IllegalAccessException 255 { 256 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 257 258 InscriptionStatus inscriptionStatus = visibility == 1 259 ? InscriptionStatus.PRIVATE 260 : (visibility == 2 261 ? InscriptionStatus.MODERATED 262 : InscriptionStatus.OPEN); 263 _projectManager.checkRightsForProjectCreation(inscriptionStatus, zoneItem.getZone().getSitemapElement()); 264 265 // Get service parameters 266 ModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 267 String titlePrefix = serviceDataHolder.getValue("titlePrefix", false, ""); 268 String urlPrefix = serviceDataHolder.getValue("urlPrefix", false, ""); 269 String[] availableLanguages = serviceDataHolder.getValue("availableLanguages"); 270 String skin = serviceDataHolder.getValue("skin", false, ""); 271 String forceAcceptCookie = serviceDataHolder.getValue("force_accept_cookies", false, ""); 272 String[] populationIds = serviceDataHolder.getValue("populationIds") != null ? serviceDataHolder.getValue("populationIds") : new String[0]; 273 274 Site catalogSite = zoneItem.getZone().getSitemapElement().getSite(); 275 276 // Check language 277 if (Arrays.stream(availableLanguages).filter(language::equals).findFirst().isEmpty()) 278 { 279 throw new IllegalArgumentException("Cannot create project with language '" + language + "' since it is not part of the available languages " + availableLanguages); 280 } 281 // Check profile 282 String defaultManagerProfile = Config.getInstance().getValue("workspaces.profile.managerdefault", false, null); 283 if (StringUtils.isBlank(defaultManagerProfile)) 284 { 285 throw new IllegalArgumentException("The general configuration parameter 'workspaces.profile.managerdefault' cannot be empty"); 286 } 287 288 // Create tags if necessary 289 Pair<List<String>, List<Map<String, Object>>> keywordsInfo = _createMissingKeywords(keywords); 290 List<String> keywordsToSet = keywordsInfo.getLeft(); 291 List<Map<String, Object>> newKeywordsInfo = keywordsInfo.getRight(); 292 293 // Create project object in repo 294 Map<String, Object> additionalValues = new HashMap<>(); 295 additionalValues.put("description", StringUtils.defaultString(description)); 296 additionalValues.put("categoryTags", Collections.singletonList(category)); 297 additionalValues.put("inscriptionStatus", inscriptionStatus.toString()); 298 additionalValues.put("defaultProfile", defaultProfile); 299 additionalValues.put("keywords", keywordsToSet); 300 additionalValues.put("language", language); 301 302 List<String> errors = new ArrayList<>(); 303 String prefixedTitle = (titlePrefix + " " + title).trim(); 304 Project project = _projectManager.createProject(_findName(prefixedTitle), 305 prefixedTitle, 306 additionalValues, 307 _withDefaultModules(modules), 308 errors); 309 310 if (!CollectionUtils.isEmpty(errors)) 311 { 312 Map<String, Object> result = new HashMap<>(); 313 result.put("success", false); 314 315 return result; 316 } 317 318 // Add site infos 319 _updateSiteInfos(project, urlPrefix, skin, forceAcceptCookie, catalogSite, illustration, language); 320 321 // Assign populations and manager 322 _assignPopulations(project, catalogSite, Set.of(populationIds)); 323 _assignManagers(project, managers, defaultManagerProfile); 324 project.saveChanges(); 325 326 Map<String, Object> result = new HashMap<>(); 327 result.put("success", true); 328 result.put("project", _detailedMyProject2json(project, zoneItem.getZone().getSitemapElement(), false, _notificationPreferenceHelper.getPausedProjects(_currentUserProvider.getUser()) == null ? null : false)); 329 result.put("keywords", newKeywordsInfo); 330 return result; 331 } 332 333 private void _assignPopulations(Project project, Site catalogSite, Set<String> serviceParameterPopulationIds) 334 { 335 Site site = project.getSite(); 336 337 // By default, copy populations from the catalog 338 Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + catalogSite.getName(), false); 339 Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + catalogSite.getName(), false); 340 341 // If populations have been specified in the service, use them 342 if (serviceParameterPopulationIds.size() > 0) 343 { 344 populations = populations.stream() 345 .filter(populationId -> serviceParameterPopulationIds.contains(populationId)) 346 .collect(Collectors.toSet()); 347 frontPopulations = frontPopulations.stream() 348 .filter(populationId -> serviceParameterPopulationIds.contains(populationId)) 349 .collect(Collectors.toSet()); 350 } 351 352 _populationContextHelper.link("/sites/" + site.getName(), populations); 353 _populationContextHelper.link("/sites-fo/" + site.getName(), frontPopulations); 354 355 356 Set<String> groupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + catalogSite.getName()); 357 Set<String> frontGroupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites-fo/" + catalogSite.getName()); 358 359 _groupDirectoryContextHelper.link("/sites/" + site.getName(), new ArrayList<>(groupDirectories)); 360 _groupDirectoryContextHelper.link("/sites-fo/" + site.getName(), new ArrayList<>(frontGroupDirectories)); 361 } 362 363 private void _assignManagers(Project project, List<String> managers, String defaultManagerProfile) 364 { 365 List<UserIdentity> projectManagers = managers.stream() 366 .map(UserIdentity::stringToUserIdentity) 367 .filter(Objects::nonNull) 368 .filter(user -> _projectManager.isUserInProjectPopulations(project, user)) 369 .collect(Collectors.toList()); 370 371 _projectMemberManager.setProjectManager(project.getName(), defaultManagerProfile, projectManagers); 372 } 373 374 private void _updateSiteInfos(Project project, String urlPrefix, String skin, String forceAcceptCookie, Site catalogSite, Object illustration, String language) 375 { 376 Site site = project.getSite(); 377 378 site.setUrl(urlPrefix + "/" + project.getName()); 379 site.setValue("skin", skin); 380 if (site.hasDefinition("force-accept-cookies")) 381 { 382 site.setValue("force-accept-cookies", forceAcceptCookie); 383 } 384 385 site.setValue("display-restricted-pages", false); 386 site.setValue("ping_activated", false); 387 site.setValue("site-mail-from", catalogSite.getValue("site-mail-from")); 388 site.setValue("site-contents-comments-postvalidation", catalogSite.getValue("site-contents-comments-postvalidation")); 389 site.setValue("color", catalogSite.getValue("color")); 390 391 _setIllustration(site, illustration); 392 393 _siteDAO.setLanguages(site, Collections.singletonList(language)); 394 395 site.saveChanges(); 396 397 Map<String, Object> eventParams = new HashMap<>(); 398 eventParams = new HashMap<>(); 399 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE, site); 400 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 401 } 402 403 private void _setIllustration(Site site, Object illustration) 404 { 405 Object illustrationObject = null; 406 try 407 { 408 illustrationObject = _getIllustrationSource(illustration); 409 if (illustrationObject instanceof Source) 410 { 411 Source illustrationSource = (Source) illustrationObject; 412 try (InputStream is = illustrationSource.getInputStream()) 413 { 414 site.setIllustration(is, illustrationSource.getMimeType(), FilenameUtils.getName(illustrationSource.getURI()), ZonedDateTime.now()); 415 } 416 } 417 else if (illustrationObject instanceof Part) 418 { 419 Part illustrationPart = (Part) illustrationObject; 420 try (InputStream is = illustrationPart.getInputStream()) 421 { 422 site.setIllustration(is, illustrationPart.getMimeType(), illustrationPart.getUploadName(), ZonedDateTime.now()); 423 } 424 } 425 } 426 catch (IOException e) 427 { 428 throw new IllegalArgumentException("Cannot not get illustration", e); 429 } 430 finally 431 { 432 if (illustrationObject instanceof Source) 433 { 434 _sourceResolver.release((Source) illustrationObject); 435 } 436 } 437 438 } 439 440 private Object _getIllustrationSource(Object illustration) throws IOException 441 { 442 if (illustration instanceof String) 443 { 444 String illustrationAsString = (String) illustration; 445 if (illustrationAsString.contains("/") || illustrationAsString.contains("\\")) 446 { 447 throw new IllegalArgumentException("Cannot choose an illustration outside the library directory"); 448 } 449 450 return _sourceResolver.resolveURI("plugin:workspaces://resources/img/catalog/library/" + illustrationAsString); 451 } 452 else if (illustration instanceof Part) 453 { 454 Part illustrationAsFile = (Part) illustration; 455 return illustrationAsFile; 456 } 457 else // boolean => unchanged 458 { 459 return null; 460 } 461 } 462 463 private Set<String> _withDefaultModules(List<String> modules) 464 { 465 Set<String> modulesToActivate = new HashSet<>(); 466 467 modulesToActivate.addAll(DEFAULT_MODULES); 468 modulesToActivate.addAll(modules); 469 470 return modulesToActivate; 471 } 472 473 private String _findName(String title) 474 { 475 String originalName = NameHelper.filterName(title); 476 String name = originalName; 477 478 int index = 2; 479 while (_projectManager.hasProject(name)) 480 { 481 name = originalName + "-" + (index++); 482 } 483 484 return name; 485 } 486 487 /** 488 * Edit an existing project 489 * @param zoneItemId The id of the zoneitem holding the catalog service 490 * @param projectId The id of the project 491 * @param title New title 492 * @param description New description 493 * @param illustration New illustration 494 * @param category New category 495 * @param keywords The project keywords 496 * @param visibility New visibility 497 * @param defaultProfile New default profile 498 * @param managers New managers 499 * @param modules New modules 500 * @return The success map with project description 501 * @throws IllegalAccessException if user has not the convenient rights 502 */ 503 @Callable 504 public Map<String, Object> editProject(String zoneItemId, String projectId, String title, String description, Object illustration, String category, List<Object> keywords, Integer visibility, String defaultProfile, List<String> managers, List<String> modules) throws IllegalAccessException 505 { 506 Project project = _resolver.resolveById(projectId); 507 if (project == null) 508 { 509 throw new IllegalArgumentException("Unable to edit a project, invalid project id received '" + projectId + "'"); 510 } 511 512 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 513 InscriptionStatus inscriptionStatus = visibility == 1 514 ? InscriptionStatus.PRIVATE 515 : (visibility == 2 516 ? InscriptionStatus.MODERATED 517 : InscriptionStatus.OPEN); 518 _projectManager.checkRightsForProjectEdition(project, inscriptionStatus, zoneItem.getZone().getSitemapElement()); 519 520 // Check profile 521 String defaultManagerProfile = Config.getInstance().getValue("workspaces.profile.managerdefault", false, null); 522 if (StringUtils.isBlank(defaultManagerProfile)) 523 { 524 throw new IllegalArgumentException("The general configuration parameter 'workspaces.profile.managerdefault' cannot be empty"); 525 } 526 527 boolean canEdit = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, project) == RightResult.RIGHT_ALLOW 528 || _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, zoneItem.getZone().getSitemapElement()) == RightResult.RIGHT_ALLOW; 529 if (!canEdit) 530 { 531 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit the project '" + projectId + "' without sufficient rights"); 532 } 533 534 // Create keywords if necessary 535 Pair<List<String>, List<Map<String, Object>>> keywordsInfo = _createMissingKeywords(keywords); 536 List<String> keywordsToSet = keywordsInfo.getLeft(); 537 List<Map<String, Object>> newKeywordsInfo = keywordsInfo.getRight(); 538 539 project.setTitle(title); 540 project.setDescription(StringUtils.defaultString(description)); 541 project.setInscriptionStatus(inscriptionStatus.toString()); 542 project.setDefaultProfile(defaultProfile); 543 project.setCategoryTags(Collections.singletonList(category)); 544 project.setKeywords(keywordsToSet.toArray(new String[keywordsToSet.size()])); 545 546 Site site = project.getSite(); 547 548 _projectManager.setProjectSiteTitle(site, project.getTitle()); 549 _setIllustration(site, illustration); 550 if (site.needsSave()) 551 { 552 site.saveChanges(); 553 554 Map<String, Object> eventParams = new HashMap<>(); 555 eventParams.put(ObservationConstants.ARGS_SITE, site); 556 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 557 } 558 559 _updateModules(project, _withDefaultModules(modules)); 560 _assignManagers(project, managers, defaultManagerProfile); 561 562 if (project.needsSave()) 563 { 564 project.saveChanges(); 565 566 // Notify observers 567 Map<String, Object> eventParams = new HashMap<>(); 568 eventParams.put(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT, project); 569 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 570 } 571 572 UserIdentity user = _currentUserProvider.getUser(); 573 574 Map<String, Object> result = new HashMap<>(); 575 result.put("success", true); 576 result.put("project", _detailedMyProject2json(project, zoneItem.getZone().getSitemapElement(), _favoritesHelper.getFavorites(user).contains(project.getName()), _notificationPreferenceHelper.getPausedProjects(user).contains(project.getName()))); 577 result.put("keywords", newKeywordsInfo); 578 return result; 579 } 580 581 private void _updateModules(Project project, Set<String> modules) 582 { 583 Set<String> modulesToActivate = new HashSet<>(modules); 584 Set<String> modulesToDeactivate = new HashSet<>(Arrays.asList(project.getModules())); 585 modulesToActivate.removeAll(new HashSet<>(Arrays.asList(project.getModules()))); 586 modulesToDeactivate.removeAll(modules); 587 588 if (!modulesToActivate.isEmpty()) 589 { 590 Map<String, Object> additionalValues = new HashMap<>(); 591 additionalValues.put("description", project.getDescription()); 592 additionalValues.put("inscriptionStatus", project.getInscriptionStatus()); 593 additionalValues.put("emailList", project.getMailingList()); 594 additionalValues.put("defaultProfile", project.getDefaultProfile()); 595 additionalValues.put("categoryTags", project.getCategories()); 596 additionalValues.put("keywords", project.getKeywords()); 597 598 Site site = project.getSite(); 599 if (site != null) 600 { 601 additionalValues.put("language", site.getSitemaps().iterator().next().getName()); 602 } 603 604 _projectManager.activateModules(project, modulesToActivate, additionalValues); 605 } 606 if (!modulesToDeactivate.isEmpty()) 607 { 608 _projectManager.deactivateModules(project, modulesToDeactivate); 609 } 610 } 611 612 /** 613 * Delete a project 614 * @param zoneItemId The id of the zoneitem holding the catalog service 615 * @param projectId The project id 616 * @return The result 617 * @throws IllegalAccessException if an error occurred 618 */ 619 @Callable 620 public Map<String, Object> deleteProject(String zoneItemId, String projectId) throws IllegalAccessException 621 { 622 Project project = _resolver.resolveById(projectId); 623 624 if (project == null) 625 { 626 throw new IllegalArgumentException("Unable to delete a project, invalid project id received '" + projectId + "'"); 627 } 628 629 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 630 boolean canDelete = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, project) == RightResult.RIGHT_ALLOW 631 || _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, zoneItem.getZone().getSitemapElement()) == RightResult.RIGHT_ALLOW; 632 if (!canDelete) 633 { 634 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete the project '" + projectId + "' without sufficient rights"); 635 } 636 637 Map<String, Object> result = new HashMap<>(); 638 result.put("sites", _projectManager.deleteProject(project)); 639 result.put("success", true); 640 return result; 641 } 642 643 /** 644 * Add the current user to the project, if the project's inscriptions are opened 645 * @param projectId The project id 646 * @return The result 647 * @throws MessagingException If an error occurred sending a notification mail to the project manager 648 * @throws IOException If an error occurred sending the email to the project's manager 649 */ 650 @Callable 651 public Map<String, Object> joinProject(String projectId) throws MessagingException, IOException 652 { 653 Map<String, Object> result = new HashMap<>(); 654 Project project = _resolver.resolveById(projectId); 655 656 if (project == null) 657 { 658 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 659 } 660 661 UserIdentity currentUser = _currentUserProvider.getUser(); 662 663 664 Site site = project.getSite(); 665 try 666 { 667 if (!_projectManager.isUserInProjectPopulations(project, currentUser)) 668 { 669 // User is not in the project site populations, cannot be added 670 result.put("success", false); 671 return result; 672 } 673 } 674 catch (IllegalArgumentException e) 675 { 676 result.put("success", false); 677 return result; 678 } 679 680 681 682 boolean success = _projectMemberManager.addProjectMember(project, currentUser); 683 684 result.put("success", success); 685 if (success) 686 { 687 String url = site.getUrl(); 688 result.put("url", url); 689 690 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 691 692 List<String> managersEmails = Arrays.stream(project.getManagers()) 693 .map(manager -> _userManager.getUser(manager)) 694 .filter(Objects::nonNull) 695 .map(User::getEmail) 696 .filter(StringUtils::isNotEmpty) 697 .collect(Collectors.toList()); 698 699 if (managersEmails.size() > 0 && mailFrom != null) 700 { 701 Map<String, I18nizableTextParameter> params = new HashMap<>(); 702 User current = _userManager.getUser(currentUser); 703 params.put("user", new I18nizableText(current != null ? current.getFullName() : currentUser.getLogin())); 704 params.put("project", new I18nizableText(project.getTitle())); 705 params.put("url", new I18nizableText(url != null ? url : "")); 706 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_TITLE", params)); 707 String textBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_BODY", params)); 708 709 for (String managerMail : managersEmails) 710 { 711 SendMailHelper.newMail() 712 .withSubject(subject) 713 .withTextBody(textBody) 714 .withSender(mailFrom) 715 .withRecipient(managerMail) 716 .sendMail(); 717 } 718 } 719 } 720 return result; 721 } 722 723 /** 724 * Send a demand to join a project to the project's manager, if the project's inscriptions are moderated 725 * @param projectId The project to join 726 * @param message A message to send to the project's manager. 727 * @return The result 728 * @throws MessagingException If an error occurred sending the email to the project's manager 729 * @throws IOException If an error occurred sending the email to the project's manager 730 */ 731 @Callable 732 public Map<String, Object> askToJoinProject(String projectId, String message) throws MessagingException, IOException 733 { 734 Map<String, Object> result = new HashMap<>(); 735 736 Project project = _resolver.resolveById(projectId); 737 if (project == null) 738 { 739 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 740 } 741 742 UserIdentity currentUser = _currentUserProvider.getUser(); 743 744 if (!_projectManager.isUserInProjectPopulations(project, currentUser)) 745 { 746 // User is not in the project site populations, cannot be added 747 result.put("success", false); 748 return result; 749 } 750 751 _sendAskToJoinMail(message, project, currentUser); 752 result.put("success", true); 753 result.put("added-notification", Config.getInstance().getValue("workspaces.member.added.send.notification")); 754 return result; 755 } 756 757 private void _sendAskToJoinMail(String message, Project project, UserIdentity joiningUser) throws MessagingException, IOException 758 { 759 String url = getAddUserUrl(project, joiningUser); 760 761 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 762 763 List<String> managersEmails = Arrays.stream(project.getManagers()) 764 .map(manager -> _userManager.getUser(manager)) 765 .filter(Objects::nonNull) 766 .map(User::getEmail) 767 .filter(StringUtils::isNotEmpty) 768 .collect(Collectors.toList()); 769 770 if (managersEmails.size() > 0 && mailFrom != null) 771 { 772 String escapedMessage = StringUtils.isEmpty(message) ? null : message.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\n", "<br/>"); 773 774 Map<String, I18nizableTextParameter> params = new HashMap<>(); 775 User current = _userManager.getUser(joiningUser); 776 params.put("user", new I18nizableText(current != null ? current.getFullName() : joiningUser.getLogin())); 777 params.put("project", new I18nizableText(project.getTitle())); 778 params.put("url", new I18nizableText(url)); 779 params.put("message", new I18nizableText(escapedMessage != null ? escapedMessage : "")); 780 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_TITLE", params)); 781 String htmlBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", escapedMessage != null ? "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_BODY" : "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_BODY_EMPTY" , params)); 782 783 SendMailHelper.newMail() 784 .withSubject(subject) 785 .withHTMLBody(htmlBody) 786 .withSender(mailFrom) 787 .withRecipients(managersEmails) 788 .sendMail(); 789 } 790 } 791 792 /** 793 * Get the absolute url to add a user to a project 794 * @param project The project 795 * @param user the identity of user to add 796 * @return the absolute page url 797 */ 798 protected String getAddUserUrl(Project project, UserIdentity user) 799 { 800 String memberPage = ""; 801 802 Set<Page> membersPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID); 803 if (!membersPages.isEmpty()) 804 { 805 memberPage = ResolveURIComponent.resolve("page", membersPages.iterator().next().getId(), false, true); 806 } 807 808 Site site = project.getSite(); 809 String siteURL = site.getUrl(); 810 String urlWithoutScheme = StringUtils.substringAfter(siteURL, "://"); 811 String relativeURL = StringUtils.contains(urlWithoutScheme, "/") ? "/" + StringUtils.substringAfter(urlWithoutScheme, "/") : ""; 812 return siteURL + "/_authenticate?requestedURL=" + URIUtils.encodeParameter(relativeURL + "/plugins/workspaces/add-member?redirect=" + URIUtils.encodeParameter(memberPage + "?added=" + URIUtils.encodeParameter(UserIdentity.userIdentityToString(user))) + "&user=" + URIUtils.encodeParameter(UserIdentity.userIdentityToString(user)) + "&project=" + URIUtils.encodeParameter(project.getName())); 813 } 814 815 /** 816 * Get the list of allowed data in the form 817 * @return the list of allowed data in the form 818 */ 819 protected String[] getAllowedFormData() 820 { 821 return __ALLOWED_FORM_DATA; 822 } 823 824 825 /** 826 * Callable to get projects of the user and the public projects he can subscribe. 827 * @param zoneItemId the zoneItemId of the catalog service, used to get allowed populations 828 * @return A map with three entries an entry for user projects, another one for public projects and finally one for the project's creation right 829 */ 830 @Callable 831 public Map<String, Object> getUserAndPublicProjects(String zoneItemId) 832 { 833 Map<String, Object> result = new HashMap<>(); 834 835 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 836 ModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 837 String[] populationIds = serviceDataHolder.getValue("populationIds") != null ? serviceDataHolder.getValue("populationIds") : new String[0]; 838 839 UserIdentity user = _currentUserProvider.getUser(); 840 841 // Check if the user is inside populations of future workspaces (only if at least one population have been selected) 842 boolean inPopulation = populationIds.length == 0 || Arrays.asList(populationIds).contains(user.getPopulationId()); 843 844 SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement(); 845 boolean canCreatePrivateProjet = inPopulation && (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/cms") == RightResult.RIGHT_ALLOW 846 || _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, sitemapElement) == RightResult.RIGHT_ALLOW); 847 boolean canCreatePublicProjetWithModeration = inPopulation && (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/cms") == RightResult.RIGHT_ALLOW 848 || _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, sitemapElement) == RightResult.RIGHT_ALLOW); 849 boolean canCreatePublicProjet = inPopulation && (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/cms") == RightResult.RIGHT_ALLOW 850 || _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, sitemapElement) == RightResult.RIGHT_ALLOW); 851 852 result.put("canCreate", canCreatePrivateProjet || canCreatePublicProjetWithModeration || canCreatePublicProjet); 853 result.put("canCreatePrivateProject", canCreatePrivateProjet); 854 result.put("canCreatePublicProjectWithModeration", canCreatePublicProjetWithModeration); 855 result.put("canCreatePublicProject", canCreatePublicProjet); 856 857 List<Map<String, Object>> userProjects = new ArrayList<>(); 858 List<Map<String, Object>> publicProjects = new ArrayList<>(); 859 860 Set<String> favorites = _favoritesHelper.getFavorites(user); 861 Set<String> pausedProjects = _notificationPreferenceHelper.getPausedProjects(user); 862 for (Project project : _projectManager.getProjects()) 863 { 864 if (_projectMembers.isProjectMember(project, user)) 865 { 866 Map<String, Object> json = _detailedMyProject2json(project, sitemapElement, favorites.contains(project.getName()), pausedProjects != null ? pausedProjects.contains(project.getName()) : null); 867 userProjects.add(json); 868 } 869 else if (project.getInscriptionStatus() != InscriptionStatus.PRIVATE && _projectManager.isUserInProjectPopulations(project, user)) 870 { 871 Map<String, Object> json = detailedProject2json(project); 872 publicProjects.add(json); 873 } 874 } 875 876 result.put("userProjects", userProjects); 877 result.put("availablePublicProjects", publicProjects); 878 879 return result; 880 } 881 882 /** 883 * Callable to get projects of the user. 884 * @return A map with the user projects 885 */ 886 @Callable 887 public List<Map<String, Object>> getUserProjects() 888 { 889 UserIdentity user = _currentUserProvider.getUser(); 890 891 List<Map<String, Object>> userProjects = new ArrayList<>(); 892 893 Set<String> favorites = _favoritesHelper.getFavorites(user); 894 Set<String> pausedProjects = _notificationPreferenceHelper.getPausedProjects(user); 895 for (Project project : _projectManager.getProjects()) 896 { 897 if (_projectMembers.isProjectMember(project, user)) 898 { 899 // Put null for the catalog page because this method is only used for mobile app and mobile app ignore canEdit and canDelete rights 900 Map<String, Object> json = _detailedMyProject2json(project, null, favorites.contains(project.getName()), pausedProjects != null ? pausedProjects.contains(project.getName()) : null); 901 userProjects.add(json); 902 } 903 } 904 905 return userProjects; 906 } 907 908 private Map<String, Object> _detailedMyProject2json(Project project, SitemapElement catalogPage, boolean isFavorite, Boolean isPaused) 909 { 910 Map<String, Object> json = detailedProject2json(project); 911 json.put("favorite", isFavorite); 912 json.put("notification", isPaused != null ? !isPaused : null); 913 boolean canEdit = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, project) == RightResult.RIGHT_ALLOW 914 || catalogPage != null && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, catalogPage) == RightResult.RIGHT_ALLOW; 915 json.put("canEdit", canEdit); 916 boolean canDelete = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, project) == RightResult.RIGHT_ALLOW 917 || catalogPage != null && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, catalogPage) == RightResult.RIGHT_ALLOW; 918 json.put("canDelete", canDelete); 919 json.put("canAccessBO", _projectManager.canAccessBO(project)); 920 return json; 921 } 922 923 /** 924 * Transform a {@link Project} into a json map 925 * @param project the project to parse 926 * @return a json map 927 */ 928 public Map<String, Object> detailedProject2json(Project project) 929 { 930 Map<String, Object> json = new HashMap<>(); 931 932 json.put("id", project.getId()); 933 json.put("name", project.getName()); 934 json.put("title", project.getTitle()); 935 json.put("url", _projectManager.getProjectUrl(project, StringUtils.EMPTY)); 936 937 json.put("defaultProfile", project.getDefaultProfile()); 938 939 Set<String> categories = project.getCategories(); 940 if (categories.size() != 1) 941 { 942 getLogger().warn("Project " + project.getTitle() + " (" + project.getId() + ") should have one and only one category"); 943 } 944 945 if (!categories.isEmpty()) 946 { 947 String c = categories.iterator().next(); 948 Category category = _categoryProviderEP.getTag(c, new HashMap<>()); 949 950 if (category != null) 951 { 952 Map<String, Object> map = new HashMap<>(); 953 map.put("id", category.getId()); 954 map.put("name", category.getName()); 955 map.put("title", category.getTitle()); 956 map.put("color", _categoryHelper.getCategoryColor(category).get("main")); 957 958 json.put("category", map); 959 } 960 } 961 962 String[] keywords = Stream.of(project.getKeywords()) 963 .filter(k -> _keywordProviderEP.hasTag(k, new HashMap<>())) 964 .toArray(String[]::new); 965 json.put("keywords", keywords); 966 967 UserIdentity[] managers = project.getManagers(); 968 if (managers.length > 0) 969 { 970 json.put("managers", Arrays.stream(managers) 971 .map(_userHelper::user2json) 972 .filter(userAsJson -> !userAsJson.equals(Collections.EMPTY_MAP)) 973 .collect(Collectors.toList())); 974 } 975 976 List<Map<String, Object>> members = new ArrayList<>(); 977 for (int i = 1; i < Math.min(managers.length, 4); i++) 978 { 979 members.add(_userHelper.user2json(managers[i])); 980 } 981 982 if (members.size() < 3) 983 { 984 List<UserIdentity> managersList = Arrays.asList(managers); 985 986 members.addAll( 987 _projectMembers.getProjectMembers(project, true) 988 .stream() 989 .map(ProjectMember::getUser) 990 .map(User::getIdentity) 991 .filter(Predicate.not(managersList::contains)) 992 .limit(3 - members.size()) 993 .map(_userHelper::user2json) 994 .collect(Collectors.toList()) 995 ); 996 } 997 998 json.put("members", members); 999 json.put("membersCount", _projectMembers.getMembersCount(project)); 1000 1001 json.put("modules", Arrays.asList(project.getModules())); 1002 1003 switch (project.getInscriptionStatus()) 1004 { 1005 case PRIVATE: json.put("visibility", 1); break; 1006 case MODERATED: json.put("visibility", 2); break; 1007 case OPEN: 1008 // fallthrought 1009 default: 1010 json.put("visibility", 3); break; 1011 } 1012 1013 json.put("description", project.getDescription()); 1014 1015 Site site = project.getSite(); 1016 if (site != null) 1017 { 1018 json.put("site", site.getName()); 1019 json.put("language", site.getSitemaps().iterator().next().getName()); 1020 1021 if (site.getIllustration() != null) 1022 { 1023 String illustration = ResolveURIComponent.resolveCroppedImage("site-parameter", site.getName() + ";illustration", 252, 389, false, true); 1024 json.put("illustration", illustration); 1025 } 1026 } 1027 1028 return json; 1029 } 1030 1031 /** 1032 * SAX a project 1033 * @param contentHandler The content handler to sax into 1034 * @param project the project 1035 * @throws SAXException if an error occurred while saxing 1036 */ 1037 public void saxProject(ContentHandler contentHandler, Project project) throws SAXException 1038 { 1039 AttributesImpl attrs = new AttributesImpl(); 1040 attrs.addCDATAAttribute("id", project.getId()); 1041 attrs.addCDATAAttribute("name", project.getName()); 1042 1043 XMLUtils.startElement(contentHandler, "project", attrs); 1044 1045 XMLUtils.createElement(contentHandler, "title", project.getTitle()); 1046 1047 XMLUtils.createElement(contentHandler, "inscriptionStatus", project.getInscriptionStatus().name()); 1048 1049 String description = project.getDescription(); 1050 if (description != null) 1051 { 1052 XMLUtils.createElement(contentHandler, "description", description); 1053 } 1054 XMLUtils.createElement(contentHandler, "url", _projectManager.getProjectUrl(project, StringUtils.EMPTY)); 1055 1056 saxCategory(contentHandler, project); 1057 1058 for (String keyword : project.getKeywords()) 1059 { 1060 XMLUtils.createElement(contentHandler, "keyword", keyword); 1061 } 1062 1063 UserIdentity[] managers = project.getManagers(); 1064 if (managers.length > 0) 1065 { 1066 for (UserIdentity userIdentity : managers) 1067 { 1068 _userHelper.saxUserIdentity(userIdentity, contentHandler, "manager"); 1069 } 1070 } 1071 1072 Site site = project.getSite(); 1073 if (site != null) 1074 { 1075 attrs.clear(); 1076 attrs.addCDATAAttribute("name", site.getName()); 1077 attrs.addCDATAAttribute("language", site.getSitemaps().iterator().next().getName()); 1078 1079 XMLUtils.createElement(contentHandler, "site", attrs); 1080 } 1081 1082 XMLUtils.endElement(contentHandler, "project"); 1083 } 1084 1085 /** 1086 * SAX the project's category 1087 * @param contentHandler the content handler to sax into 1088 * @param project the project 1089 * @throws SAXException if an error occurred while saxing 1090 */ 1091 public void saxCategory(ContentHandler contentHandler, Project project) throws SAXException 1092 { 1093 saxCategory(contentHandler, project, "category"); 1094 } 1095 1096 /** 1097 * SAX the project's category 1098 * @param contentHandler the content handler to sax into 1099 * @param project the project 1100 * @param tagName the tag name for category 1101 * @throws SAXException if an error occurred while saxing 1102 */ 1103 public void saxCategory(ContentHandler contentHandler, Project project, String tagName) throws SAXException 1104 { 1105 Set<String> categories = project.getCategories(); 1106 if (!categories.isEmpty()) 1107 { 1108 String c = categories.iterator().next(); 1109 Category category = _categoryProviderEP.getTag(c, new HashMap<>()); 1110 1111 if (category != null) 1112 { 1113 AttributesImpl attrs = new AttributesImpl(); 1114 attrs.addCDATAAttribute("id", category.getId()); 1115 attrs.addCDATAAttribute("name", category.getName()); 1116 attrs.addCDATAAttribute("color", _categoryHelper.getCategoryColor(category).get("main")); 1117 1118 XMLUtils.startElement(contentHandler, tagName, attrs); 1119 category.getTitle().toSAX(contentHandler); 1120 XMLUtils.endElement(contentHandler, tagName); 1121 } 1122 } 1123 } 1124}