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