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.report; 017 018import java.io.IOException; 019import java.text.SimpleDateFormat; 020import java.time.format.DateTimeFormatter; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Calendar; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.List; 027import java.util.Objects; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import javax.jcr.Node; 032import javax.jcr.NodeIterator; 033import javax.jcr.Repository; 034import javax.jcr.RepositoryException; 035import javax.jcr.Session; 036import javax.jcr.query.Query; 037 038import org.apache.avalon.framework.context.Context; 039import org.apache.avalon.framework.context.ContextException; 040import org.apache.avalon.framework.context.Contextualizable; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.cocoon.ProcessingException; 044import org.apache.cocoon.components.ContextHelper; 045import org.apache.cocoon.environment.Request; 046import org.apache.cocoon.generation.ServiceableGenerator; 047import org.apache.cocoon.xml.XMLUtils; 048import org.apache.commons.lang3.StringUtils; 049import org.xml.sax.SAXException; 050 051import org.ametys.cms.data.ContentValue; 052import org.ametys.cms.repository.Content; 053import org.ametys.cms.tag.Tag; 054import org.ametys.core.group.GroupManager; 055import org.ametys.core.user.User; 056import org.ametys.core.user.UserIdentity; 057import org.ametys.core.user.UserManager; 058import org.ametys.core.util.I18nUtils; 059import org.ametys.plugins.repository.AmetysRepositoryException; 060import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 061import org.ametys.plugins.repository.events.EventType; 062import org.ametys.plugins.repository.provider.AbstractRepository; 063import org.ametys.plugins.repository.query.SortCriteria; 064import org.ametys.plugins.userdirectory.UserDirectoryHelper; 065import org.ametys.plugins.workspaces.keywords.KeywordsDAO; 066import org.ametys.plugins.workspaces.members.ProjectMemberManager; 067import org.ametys.plugins.workspaces.project.ProjectManager; 068import org.ametys.plugins.workspaces.project.objects.Project; 069import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus; 070import org.ametys.runtime.i18n.I18nizableText; 071import org.ametys.web.repository.site.Site; 072import org.ametys.web.repository.site.SiteManager; 073import org.ametys.web.repository.sitemap.Sitemap; 074 075/** 076 * Report projects with theirs managers/users 077 */ 078public class ReportGenerator extends ServiceableGenerator implements Contextualizable 079{ 080 /** Time format for csv export */ 081 protected static final String __DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; 082 083 /** Zoned dateTime formatter */ 084 protected static final DateTimeFormatter __ZONED_DATE_FORMATER = DateTimeFormatter.ofPattern(__DATE_TIME_FORMAT); 085 086 /** DateTime formatter */ 087 protected static final SimpleDateFormat __DATE_FORMATER = new SimpleDateFormat(__DATE_TIME_FORMAT); 088 089 090 /** number of columns for a project */ 091 protected static final int __REPORT_COLUMNS_SIZE_PROJECT = 8; 092 093 /** number of columns for a member */ 094 protected static final int __REPORT_COLUMNS_SIZE_MEMBER = 8; 095 096 /** Report Project Manager */ 097 protected ReportHelper _reportProjectManager; 098 099 /** The project manager */ 100 protected ProjectManager _projectManager; 101 102 /** Site manager */ 103 protected SiteManager _siteManager; 104 105 /** Repository */ 106 protected Repository _repository; 107 108 /** User Manager */ 109 protected UserManager _userManager; 110 111 /** Group Manager */ 112 protected GroupManager _groupManager; 113 114 /** Project Member Manager */ 115 protected ProjectMemberManager _projectMemberManager; 116 117 /** User Directory Helper */ 118 protected UserDirectoryHelper _userDirectoryHelper; 119 120 /** I18n Utils */ 121 protected I18nUtils _i18nUtils; 122 123 /** The keywords DAO */ 124 protected KeywordsDAO _keywordsDAO; 125 126 /** Avalon context */ 127 protected Context _context; 128 129 @Override 130 public void contextualize(Context context) throws ContextException 131 { 132 _context = context; 133 } 134 135 @Override 136 public void service(ServiceManager smanager) throws ServiceException 137 { 138 super.service(smanager); 139 _reportProjectManager = (ReportHelper) smanager.lookup(ReportHelper.ROLE); 140 _projectManager = (ProjectManager) smanager.lookup(ProjectManager.ROLE); 141 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 142 _repository = (Repository) smanager.lookup(AbstractRepository.ROLE); 143 _userManager = (UserManager) smanager.lookup(UserManager.ROLE); 144 _groupManager = (GroupManager) smanager.lookup(GroupManager.ROLE); 145 _projectMemberManager = (ProjectMemberManager) smanager.lookup(ProjectMemberManager.ROLE); 146 _userDirectoryHelper = (UserDirectoryHelper) smanager.lookup(UserDirectoryHelper.ROLE); 147 _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE); 148 _keywordsDAO = (KeywordsDAO) manager.lookup(KeywordsDAO.ROLE); 149 } 150 151 public void generate() throws IOException, SAXException, ProcessingException 152 { 153 Request request = ContextHelper.getRequest(_context); 154 155 String lang = parameters.getParameter("lang", null); 156 String[] projectsValues = request.getParameterValues("project"); 157 String[] categoriesValues = request.getParameterValues("category"); 158 159 // Filters because the js may send the parameter empty (which will create a non-emply list with an empty string) 160 List<String> projects = projectsValues == null ? Collections.EMPTY_LIST : Arrays.asList(projectsValues) 161 .stream() 162 .filter(StringUtils::isNotBlank) 163 .collect(Collectors.toList()); 164 165 List<String> categories = categoriesValues == null ? Collections.EMPTY_LIST : Arrays.asList(categoriesValues) 166 .stream() 167 .filter(StringUtils::isNotBlank) 168 .collect(Collectors.toList()); 169 170 boolean reportWithMembers = "true".equals(request.getParameter("with-members")); 171 boolean reportWithManagers = "true".equals(request.getParameter("with-managers")); 172 173 contentHandler.startDocument(); 174 List<Project> availableProjects = _reportProjectManager.getAvailableProjects(projects, categories); 175 if (availableProjects != null && !availableProjects.isEmpty()) 176 { 177 XMLUtils.createElement(contentHandler, "text", generateProjects(availableProjects, reportWithMembers, reportWithManagers, lang)); 178 } 179 else 180 { 181 XMLUtils.createElement(contentHandler, "text"); 182 } 183 contentHandler.endDocument(); 184 } 185 186 /** 187 * Generate the list of projects 188 * @param projects list of projects to generate 189 * @param reportWithMembers also report members 190 * @param reportWithManagers also report managers 191 * @param lang language used to parse agents (if not available, another langage will still be used) 192 * @return A String that will represent the csv 193 */ 194 protected String generateProjects(List<Project> projects, boolean reportWithMembers, boolean reportWithManagers, String lang) 195 { 196 // Languages used to use one of them on each content to report 197 List<String> langages = getCatalogSiteLangages(); 198 199 StringBuilder sb = new StringBuilder(); 200 sb.append("\ufeff"); // BOM character 201 sb.append("\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_TYPE", lang) + "\";"); 202 sb.append(getProjectCsvHeader(reportWithMembers, reportWithManagers, lang)); 203 204 sb.append("\n"); 205 206 // categories 207 sb.append(projects.stream() 208 .map(project -> translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT", lang) + ";" + projectToCsv(project, reportWithMembers, reportWithManagers, lang, langages)) 209 .collect(Collectors.joining("\n"))); 210 211 return sb.toString(); 212 } 213 214 /** 215 * Get the languages of the catalog site 216 * @return the list of catalog site languages 217 */ 218 protected List<String> getCatalogSiteLangages() 219 { 220 String catalogSiteName = _projectManager.getCatalogSiteName(); 221 if (StringUtils.isNotEmpty(catalogSiteName)) 222 { 223 Site siteCatalog = _siteManager.getSite(catalogSiteName); 224 if (siteCatalog != null) 225 { 226 return siteCatalog.getSitemaps().stream() 227 .map(Sitemap::getSitemapName) 228 .collect(Collectors.toList()); 229 } 230 } 231 return null; 232 } 233 234 /** 235 * Format a String to csv 236 * @param line strings to transform 237 * @return a string representing the input strings 238 */ 239 protected static String formatToCsv(String... line) 240 { 241 return Arrays.stream(line).collect(Collectors.joining("\";\"", "\"", "\"")); 242 } 243 244 /** 245 * Generate the header for projects 246 * @param reportWithMembers also report members 247 * @param reportWithManagers also report managers 248 * @param lang language to use to create header 249 * @return a String representing the csv header 250 */ 251 protected String getProjectCsvHeader(boolean reportWithMembers, boolean reportWithManagers, String lang) 252 { 253 StringBuilder sb = new StringBuilder(formatToCsv( 254 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_NAME", lang), 255 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_DESC", lang), 256 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_CATEGORY", lang), 257 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_CREATION_DATE", lang), 258 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_ACTIVITY_DATE", lang), 259 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_KEYWONDS", lang), 260 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MANAGERS", lang), 261 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_VISIBILITY", lang))); 262 263 if (reportWithMembers || reportWithManagers) 264 { 265 sb.append(";"); 266 sb.append(getMemberCsvHeader(lang)); 267 } 268 269 270 return sb.toString(); 271 } 272 273 private String getMemberCsvHeader(String lang) 274 { 275 return formatToCsv( 276 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_TITLE", lang), 277 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_FIRSTNAME", lang), 278 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_LASTNAME", lang), 279 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_FUNCTION", lang), 280 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_ORGANISATION", lang), 281 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_PHONE", lang), 282 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_MAIL", lang), 283 translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_MEMBER_SKILLS", lang)); 284 } 285 286 private String projectToCsv(Project project, boolean reportWithMembers, boolean reportWithManagers, String lang, List<String> langages) 287 { 288 Set<String> projectCategories = project.getCategories(); 289 String mainCategory = projectCategories.size() > 0 ? projectCategories.iterator().next() : ""; 290 291 String lastActivityDate = getProjectLastActivityDate(project); 292 293 String projectmanager = Arrays.stream(project.getManagers()) 294 .map(_userManager::getUser) 295 .filter(Objects::nonNull) 296 .map(user -> user.getFullName() + " (" + user.getIdentity().getPopulationId() + ")") 297 .collect(Collectors.joining(",")); 298 299 InscriptionStatus inscriptionStatus = project.getInscriptionStatus(); 300 String inscriptionStatusString = InscriptionStatus.PRIVATE.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_PRIVATE", lang) 301 : InscriptionStatus.MODERATED.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_MODERATED", lang) 302 : InscriptionStatus.OPEN.equals(inscriptionStatus) ? translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_INSCRIPTION_STATUS_OPEN", lang) : ""; 303 304 305 StringBuilder sb = new StringBuilder(formatToCsv( 306 project.getTitle(), 307 StringUtils.defaultString(project.getDescription(), "").replaceAll("[\r\n]+", " / "), 308 mainCategory, 309 __ZONED_DATE_FORMATER.format(project.getCreationDate()), 310 lastActivityDate, 311 getKeywords(project, lang), 312 projectmanager, 313 inscriptionStatusString 314 )); 315 316 int projectPadding = (reportWithMembers || reportWithManagers) ? __REPORT_COLUMNS_SIZE_MEMBER : 0; 317 318 sb.append(StringUtils.repeat(";-", projectPadding)); 319 320 if (reportWithManagers) 321 { 322 // project managers 323 String managerPrefix = "\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MANAGER", lang) + "\";\"" + project.getTitle() + "\"" + StringUtils.repeat(";-", __REPORT_COLUMNS_SIZE_PROJECT - 1) + ";"; 324 325 String managersCsv = Arrays.stream(project.getManagers()) 326 .map(userIdentity -> createProjectMemberFromUser(userIdentity, lang, langages)) 327 .filter(Objects::nonNull) 328 .map(ProjectMember::toCsv) 329 .map(managerCsv -> managerPrefix + managerCsv) 330 .collect(Collectors.joining("\n")); 331 if (StringUtils.isNotEmpty(managersCsv)) 332 { 333 sb.append("\n"); 334 sb.append(managersCsv); 335 } 336 } 337 338 if (reportWithMembers) 339 { 340 // project members 341 String memberPrefix = "\"" + translateKey("PLUGIN_WORKSPACES_SERVICE_REPORTS_CSV_HEADER_PROJECT_MEMBER", lang) + "\";\"" + project.getTitle() + "\"" + StringUtils.repeat(";-", __REPORT_COLUMNS_SIZE_PROJECT - 1) + ";"; 342 343 String projectMembersCsv = getProjectMembersCsv(project, lang, langages).stream() 344 .map(memberCsv -> memberPrefix + memberCsv) 345 .collect(Collectors.joining("\n")); 346 347 if (StringUtils.isNotEmpty(projectMembersCsv)) 348 { 349 sb.append("\n"); 350 sb.append(projectMembersCsv); 351 } 352 } 353 354 355 356 return sb.toString(); 357 } 358 359 /** 360 * Get the project's keywords 361 * @param project the project 362 * @param lang the current language 363 * @return the keywords as label 364 */ 365 protected String getKeywords(Project project, String lang) 366 { 367 List<String> keywordLabels = new ArrayList<>(); 368 369 for (String keyword : project.getKeywords()) 370 { 371 Tag tag = _keywordsDAO.getTag(keyword, Collections.emptyMap()); 372 if (tag != null) 373 { 374 String title = _i18nUtils.translate(tag.getTitle(), lang); 375 keywordLabels.add(title); 376 } 377 } 378 379 return StringUtils.join(keywordLabels, ","); 380 } 381 382 /** 383 * Get the last activity date of the project 384 * @param project project to analyze 385 * @return a formatted date 386 */ 387 protected String getProjectLastActivityDate(Project project) 388 { 389 SortCriteria sortCriteria = new SortCriteria(); 390 sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false); 391 String xpathQuery = "//element(*, ametys:event)[@ametys:projectName = '" + project.getName() + "']" + sortCriteria.build(); 392 393 Session session = null; 394 try 395 { 396 session = _repository.login(); 397 @SuppressWarnings("deprecation") 398 Query query = session.getWorkspace().getQueryManager().createQuery(xpathQuery, Query.XPATH); 399 NodeIterator nodes = query.execute().getNodes(); 400 if (nodes.hasNext()) 401 { 402 Node node = (Node) nodes.next(); 403 Calendar date = node.getProperty("ametys:date").getValue().getDate(); 404 return __DATE_FORMATER.format(date.getTime()); 405 } 406 } 407 catch (RepositoryException ex) 408 { 409 throw new AmetysRepositoryException("An error occured executing the JCR query : " + xpathQuery, ex); 410 } 411 finally 412 { 413 if (session != null) 414 { 415 session.logout(); 416 } 417 } 418 419 return ""; 420 } 421 422 /** 423 * Get a {@link ProjectMember} from a user 424 * @param userIdentity user 425 * @param lang language to use to parse user 426 * @param projectLanguages languages of the catalog site 427 * @return the user as a {@link ProjectMember} 428 */ 429 protected ProjectMember createProjectMemberFromUser(UserIdentity userIdentity, String lang, List<String> projectLanguages) 430 { 431 User user = _userManager.getUser(userIdentity); 432 if (user == null) 433 { 434 return null; 435 } 436 Content userContent = null; 437 if (StringUtils.isNotEmpty(lang)) 438 { 439 userContent = _userDirectoryHelper.getUserContent(userIdentity, lang); 440 } 441 442 // If requested language is not available, find one that is available 443 if (userContent == null) 444 { 445 userContent = projectLanguages != null ? projectLanguages.stream() 446 .map(l -> _userDirectoryHelper.getUserContent(userIdentity, l)) 447 .filter(Objects::nonNull) 448 .findFirst() 449 .orElse(null) : null; 450 } 451 452 if (userContent == null) 453 { 454 return new ProjectMember(UserIdentity.userIdentityToString(userIdentity), 455 "", 456 user.getFirstName(), 457 user.getLastName(), 458 "", 459 "", 460 "", 461 StringUtils.defaultIfEmpty(user.getEmail(), ""), 462 ""); 463 } 464 465 ModelAwareDataHolder dataHolder = userContent.getDataHolder(); 466 ContentValue[] skillsContents = dataHolder.getValue("skills"); 467 String skills = skillsContents != null ? Arrays.stream(skillsContents) 468 .filter(Objects::nonNull) 469 .map(ContentValue::getContent) 470 .filter(Objects::nonNull) 471 .map(skill -> (String) skill.getDataHolder().getValue("title")) 472 .collect(Collectors.joining(",")) 473 : ""; 474 475 476 return new ProjectMember(UserIdentity.userIdentityToString(userIdentity), 477 StringUtils.defaultIfEmpty(dataHolder.getValue("title"), ""), 478 user.getFirstName(), 479 user.getLastName(), 480 StringUtils.defaultIfEmpty(dataHolder.getValue("function"), ""), 481 StringUtils.defaultIfEmpty(dataHolder.getValue("organisation"), ""), 482 StringUtils.defaultIfEmpty(dataHolder.getValue("phone"), ""), 483 StringUtils.defaultIfEmpty(user.getEmail(), ""), 484 skills); 485 } 486 487 /** 488 * Get the csv for members 489 * @param project project to use 490 * @param lang language to use to parse user 491 * @param languages languages of the catalog site 492 * @return users as csv 493 */ 494 protected List<String> getProjectMembersCsv(Project project, String lang, List<String> languages) 495 { 496 497 498 return _projectMemberManager.getProjectMembers(project, true, false) 499 .stream() 500 .map(member -> createProjectMemberFromUser(member.getUser().getIdentity(), lang, languages)) 501 .sorted(Comparator.comparing(ProjectMember::getSortableName)) 502 .map(ProjectMember::toCsv) 503 .collect(Collectors.toList()); 504 } 505 506 /** 507 * Small shortcut to translate a key 508 * @param key i18n key 509 * @param lang requested language 510 * @return the translated key 511 */ 512 protected String translateKey(String key, String lang) 513 { 514 I18nizableText i18n = new I18nizableText("plugin.workspaces", key); 515 return _i18nUtils.translate(i18n, lang); 516 } 517 518 static class ProjectMember 519 { 520 private String _id; 521 private String _title; 522 private String _firstname; 523 private String _lastname; 524 private String _role; 525 private String _structure; 526 private String _telephone; 527 private String _email; 528 private String _skills; 529 530 ProjectMember(String id, String title, String firstname, String lastname, String role, String structure, String telephone, String email, String skills) 531 { 532 _id = id; 533 _title = title; 534 _firstname = firstname; 535 _lastname = lastname; 536 _role = role; 537 _structure = structure; 538 _telephone = telephone; 539 _email = email; 540 _skills = skills; 541 } 542 543 public String getId() 544 { 545 return _id; 546 } 547 548 public String getSortableName() 549 { 550 return _lastname + " " + _firstname; 551 } 552 553 @Override 554 public boolean equals(Object obj) 555 { 556 return _id != null && obj instanceof ProjectMember && _id.equals(((ProjectMember) obj).getId()); 557 } 558 559 /** 560 * Format a {@link ProjectMember} to a csv line 561 * @return a String representing a {@link ProjectMember} in a csv format 562 */ 563 public String toCsv() 564 { 565 return formatToCsv(_title, 566 _firstname, 567 _lastname, 568 _role, 569 _structure, 570 _telephone, 571 _email, 572 _skills); 573 } 574 575 @Override 576 public int hashCode() 577 { 578 return _id.hashCode(); 579 } 580 } 581}