001/* 002 * Copyright 2018 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.odfpilotage.report; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.nio.charset.StandardCharsets; 025import java.time.LocalDate; 026import java.time.format.DateTimeFormatter; 027import java.util.ArrayList; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Set; 033import java.util.zip.ZipEntry; 034import java.util.zip.ZipOutputStream; 035 036import org.apache.avalon.framework.activity.Initializable; 037import org.apache.avalon.framework.configuration.Configurable; 038import org.apache.avalon.framework.configuration.Configuration; 039import org.apache.avalon.framework.configuration.ConfigurationException; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043import org.apache.commons.io.FileUtils; 044import org.apache.commons.io.IOUtils; 045import org.apache.commons.lang.StringUtils; 046import org.apache.excalibur.source.Source; 047import org.apache.excalibur.source.SourceResolver; 048 049import org.ametys.cms.FilterNameHelper; 050import org.ametys.core.user.User; 051import org.ametys.core.user.UserIdentity; 052import org.ametys.core.user.UserManager; 053import org.ametys.core.util.I18nUtils; 054import org.ametys.core.util.mail.SendMailHelper; 055import org.ametys.odf.ODFHelper; 056import org.ametys.odf.enumeration.OdfReferenceTableHelper; 057import org.ametys.plugins.odfpilotage.helper.PilotageHelper; 058import org.ametys.plugins.odfpilotage.helper.ReportHelper; 059import org.ametys.plugins.odfpilotage.schedulable.AbstractReportSchedulable; 060import org.ametys.plugins.repository.AmetysObjectResolver; 061import org.ametys.runtime.config.Config; 062import org.ametys.runtime.i18n.I18nizableText; 063import org.ametys.runtime.plugin.component.AbstractLogEnabled; 064import org.ametys.runtime.plugin.component.PluginAware; 065 066import com.google.common.collect.ImmutableList; 067import com.google.gson.Gson; 068import com.google.gson.GsonBuilder; 069 070import jakarta.mail.MessagingException; 071 072/** 073 * The abstract class for pilotage reports. 074 */ 075public abstract class AbstractPilotageReport extends AbstractLogEnabled implements PilotageReport, Serviceable, Initializable, PluginAware, Configurable 076{ 077 /** Filename of the manifest to describe the ZIP content */ 078 public static final String MANIFEST_FILENAME = "manifest.json"; 079 080 private static final Gson __GSON = new GsonBuilder() 081 .setPrettyPrinting() 082 .disableHtmlEscaping() 083 .create(); 084 085 /** 086 * The enumerator for different pilotage report status 087 */ 088 public enum PilotageReportStatus 089 { 090 /** If the report has failed */ 091 FAIL, 092 /** If the report is a success */ 093 SUCCESS, 094 /** If there are no file in the report */ 095 NO_FILE 096 } 097 098 /** The source resolver */ 099 protected SourceResolver _sourceResolver; 100 101 /** The pilotage helper */ 102 protected PilotageHelper _pilotageHelper; 103 104 /** The ametys object resolver */ 105 protected AmetysObjectResolver _resolver; 106 107 /** The report helper */ 108 protected ReportHelper _reportHelper; 109 110 /** The ODF enumeration helper */ 111 protected OdfReferenceTableHelper _refTableHelper; 112 113 /** The ODF helper */ 114 protected ODFHelper _odfHelper; 115 116 /** The user manager */ 117 protected UserManager _userManager; 118 119 /** The I18N utils */ 120 protected I18nUtils _i18nUtils; 121 122 /** The tmp folder */ 123 protected File _tmpFolder; 124 125 /** The current date formatted to yyyy-MM-dd */ 126 protected String _currentFormattedDate; 127 128 private String _id; 129 private I18nizableText _label; 130 private String _pluginName; 131 private String _mailFrom; 132 private String _outputFormat; 133 134 135 @Override 136 public void initialize() throws Exception 137 { 138 _tmpFolder = new File(_reportHelper.getTmpPilotageFolder(), getType()); 139 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 140 } 141 142 @Override 143 public void setPluginInfo(String pluginName, String featureName, String id) 144 { 145 _pluginName = pluginName; 146 _id = id; 147 } 148 149 @Override 150 public void configure(Configuration configuration) throws ConfigurationException 151 { 152 _label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + _pluginName); 153 } 154 155 @Override 156 public void service(ServiceManager manager) throws ServiceException 157 { 158 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 159 _pilotageHelper = (PilotageHelper) manager.lookup(PilotageHelper.ROLE); 160 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 161 _reportHelper = (ReportHelper) manager.lookup(ReportHelper.ROLE); 162 _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 163 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 164 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 165 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 166 } 167 168 @Override 169 public String getId() 170 { 171 return _id; 172 } 173 174 @Override 175 public I18nizableText getLabel() 176 { 177 return _label; 178 } 179 180 @Override 181 public boolean supports(AbstractReportSchedulable schedulable) 182 { 183 if (schedulable.forGenericReports() == isGeneric() && isSupportedTarget(schedulable.getTarget())) 184 { 185 return isCompatibleSchedulable(schedulable); 186 } 187 188 return false; 189 } 190 191 /** 192 * Most of reports are generic. This method can be overridden. 193 * @return <code>true</code> if the current report is generic, <code>false</code> otherwise 194 */ 195 public boolean isGeneric() 196 { 197 return true; 198 } 199 200 /** 201 * Returns <code>true</code> if the target is supported by the report. 202 * @param target The target to test 203 * @return <code>true</code> if the target is supported, <code>false</code> otherwise 204 */ 205 protected abstract boolean isSupportedTarget(PilotageReportTarget target); 206 207 /** 208 * Check if the given schedulable is compatible with the current 209 * @param schedulable The schedulable to test 210 * @return <code>true</code> if the schedulable is compatible with the report 211 */ 212 protected boolean isCompatibleSchedulable(AbstractReportSchedulable schedulable) 213 { 214 return true; 215 } 216 217 /** 218 * Launch a report generation on an orgunit. 219 * @param reportParameters The report parameters 220 * @return the name of the generated file 221 * @throws Exception if an exception occurs 222 */ 223 protected abstract String launchByOrgUnit(Map<String, String> reportParameters) throws Exception; 224 225 /** 226 * Launch a report generation on a program. 227 * @param reportParameters The report parameters 228 * @return the name of the generated file 229 * @throws Exception if an exception occurs 230 */ 231 protected abstract String launchByProgram(Map<String, String> reportParameters) throws Exception; 232 233 /** 234 * Get the name of the report 235 * @return The report name 236 */ 237 protected abstract String getType(); 238 239 /** 240 * Get the plugin name to build the pipeline. 241 * @return The plugin name 242 */ 243 protected String getPluginName() 244 { 245 return _pluginName; 246 } 247 248 /** 249 * Get the output format of the report. 250 * @return The output format 251 */ 252 protected String getOutputFormat() 253 { 254 return _outputFormat; 255 } 256 257 /** 258 * Get the list of supported output formats 259 * @return A {@link Set} of supported output formats 260 */ 261 protected Set<String> getSupportedOutputFormats() 262 { 263 return Set.of(OUTPUT_FORMAT_DOC, OUTPUT_FORMAT_XLS); 264 } 265 266 /** 267 * Get the output format of the report. 268 * @return The output format 269 */ 270 protected boolean isSupportedFormat() 271 { 272 return getSupportedOutputFormats().contains(getOutputFormat()); 273 } 274 275 /** 276 * Build the pipeline to launch the transformation. 277 * @param outputFolderName The name of the output folder name 278 * @return The pipeline for transformation 279 */ 280 protected String getPipeline(String outputFolderName) 281 { 282 return "report/" + getType() + "/" + outputFolderName; 283 } 284 285 @Override 286 public synchronized void launch(PilotageReportTarget target, Map<String, String> reportParameters, UserIdentity user) 287 { 288 getLogger().info("Début du rapport de pilotage"); 289 long time_0 = System.currentTimeMillis(); 290 291 String contextName = null; 292 try 293 { 294 FileUtils.deleteQuietly(_tmpFolder); 295 _tmpFolder.mkdir(); 296 _currentFormattedDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); 297 _outputFormat = reportParameters.get(PARAMETER_OUTPUT_FORMAT); 298 299 if (!isSupportedFormat()) 300 { 301 throw new UnsupportedOperationException("Impossible to launch the report '" + getType() + "' with the output format '" + getOutputFormat() + "'."); 302 } 303 304 switch (target) 305 { 306 case PROGRAM: 307 contextName = launchByProgram(reportParameters); 308 break; 309 case ORGUNIT: 310 contextName = launchByOrgUnit(reportParameters); 311 break; 312 default: 313 break; 314 } 315 } 316 catch (Exception e) 317 { 318 getLogger().error("Erreur d'écriture du rapport '{}'.", getType(), e); 319 } 320 finally 321 { 322 PilotageFile pilotageFile = new PilotageFile(PilotageReportStatus.FAIL, null); 323 try 324 { 325 pilotageFile = createZipFile(_tmpFolder, target, reportParameters, contextName); 326 } 327 catch (Exception e) 328 { 329 getLogger().error("Une erreur est survenue lors de la compression des rapports.", e); 330 } 331 finally 332 { 333 sendMail(pilotageFile, user); 334 335 FileUtils.deleteQuietly(_tmpFolder); 336 _currentFormattedDate = null; 337 338 long time_1 = System.currentTimeMillis(); 339 getLogger().info("Calcul et écriture du rapport de pilotage effectué en {} ms.", time_1 - time_0); 340 } 341 } 342 } 343 344 /** 345 * Convert the report from XML to the required format 346 * @param outputFolder folder where the file will stay temporarily 347 * @param fileName the filename without the extension 348 * @param xmlFile the file to be converted 349 * @throws IOException if an error occurs 350 */ 351 protected void convertReport(File outputFolder, String fileName, File xmlFile) throws IOException 352 { 353 String completeFileName = fileName + "." + getOutputFormat(); 354 355 Source source = null; 356 try 357 { 358 359 // Transform XML to configured output format 360 source = _sourceResolver.resolveURI("cocoon://_plugins/odf-pilotage/" + getPipeline(outputFolder.getName()) + "/" + completeFileName, null, Map.of("reportPluginName", getPluginName())); 361 try (InputStream is = source.getInputStream()) 362 { 363 // Delete existing file 364 File file = new File(outputFolder, completeFileName); 365 FileUtils.deleteQuietly(file); 366 file.createNewFile(); 367 368 // Save file 369 try (OutputStream os = new FileOutputStream(file)) 370 { 371 IOUtils.copy(is, os); 372 } 373 } 374 } 375 finally 376 { 377 if (source != null) 378 { 379 _sourceResolver.release(source); 380 } 381 } 382 } 383 384 /** 385 * Compress a folder to zip format. 386 * @param folderToZip the folder to be compressed 387 * @param target The target of the report 388 * @param reportParameters The report parameters 389 * @param contextName the name of the report context 390 * @return The pilotage file 391 * @throws IOException if an exception occurs 392 */ 393 protected PilotageFile createZipFile(File folderToZip, PilotageReportTarget target, Map<String, String> reportParameters, String contextName) throws IOException 394 { 395 if (StringUtils.isEmpty(contextName)) 396 { 397 return new PilotageFile(PilotageReportStatus.FAIL, null); 398 } 399 400 File zipFile = new File(_reportHelper.getPilotageFolder(), _buildZipName(contextName)); 401 402 FileUtils.deleteQuietly(zipFile); 403 File[] files = folderToZip.listFiles(); 404 405 if (files.length == 0) 406 { 407 getLogger().warn("Aucun fichier généré"); 408 return new PilotageFile(PilotageReportStatus.NO_FILE, null); 409 } 410 411 getLogger().info("Création de l'archive"); 412 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) 413 { 414 // Ajout du manifest.json 415 addManifest(zos, target, reportParameters); 416 417 // Ajout des fichiers générés 418 for (File file : files) 419 { 420 try (FileInputStream is = new FileInputStream(file)) 421 { 422 zos.putNextEntry(new ZipEntry(file.getName())); 423 is.transferTo(zos); 424 } 425 finally 426 { 427 zos.closeEntry(); 428 } 429 } 430 } 431 432 return new PilotageFile(PilotageReportStatus.SUCCESS, zipFile); 433 } 434 435 /** 436 * Add the manifest to JSON format to the ZIP. 437 * @param zos The ZIP output stream 438 * @param target The target of the report 439 * @param reportParameters The report parameters 440 * @throws IOException if an exception occurs 441 */ 442 protected void addManifest(ZipOutputStream zos, PilotageReportTarget target, Map<String, String> reportParameters) throws IOException 443 { 444 Map<String, Object> manifestData = new HashMap<>(); 445 manifestData.put("type", getId()); 446 manifestData.put("date", _currentFormattedDate); 447 manifestData.put("target", target.name().toLowerCase()); 448 manifestData.putAll(reportParameters); 449 450 String json = __GSON.toJson(manifestData); 451 try 452 { 453 zos.putNextEntry(new ZipEntry(MANIFEST_FILENAME)); 454 IOUtils.write(json, zos, StandardCharsets.UTF_8); 455 } 456 finally 457 { 458 zos.closeEntry(); 459 } 460 } 461 462 /** 463 * Send a mail with the ZIP file as attachment at the end of the report generation. 464 * @param file the pilotage file 465 * @param user the recipient if he has an email 466 */ 467 protected void sendMail(PilotageFile file, UserIdentity user) 468 { 469 String recipient = Optional.ofNullable(user) 470 .map(_userManager::getUser) 471 .map(User::getEmail) 472 .filter(StringUtils::isNotEmpty) 473 .orElse(null); 474 475 if (recipient != null) 476 { 477 String subject = getMailSubject(); 478 String body = getMailBody(file.getStatus()); 479 try 480 { 481 File zipFile = file.getZipFile(); 482 List<File> attachments = new ArrayList<>(); 483 if (zipFile != null && zipFile.exists()) 484 { 485 attachments.add(zipFile); 486 } 487 488 getLogger().info("Envoi du rapport par mail"); 489 SendMailHelper.newMail() 490 .withSubject(subject) 491 .withTextBody(body) 492 .withAttachments(attachments) 493 .withRecipient(recipient) 494 .withSender(_mailFrom) 495 .sendMail(); 496 } 497 catch (MessagingException | IOException e) 498 { 499 getLogger().warn("Fail to send email to {}", recipient, e); 500 } 501 } 502 } 503 504 /** 505 * The mail subject. 506 * @return The subject of the mail 507 */ 508 protected String getMailSubject() 509 { 510 return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", "PLUGINS_ODF_PILOTAGE_MAIL_SUBJECT", ImmutableList.of(getReportName()))); 511 } 512 513 /** 514 * The mail body. 515 * @param status the status of the pilotage report 516 * @return The body of the mail 517 */ 518 protected String getMailBody(PilotageReportStatus status) 519 { 520 String key = "PLUGINS_ODF_PILOTAGE_MAIL_BODY_" + status.name(); 521 return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", key, ImmutableList.of(getReportName()))); 522 } 523 524 /** 525 * The report name to add in the mail. 526 * @return The report name 527 */ 528 protected String getReportName() 529 { 530 return _i18nUtils.translate(getLabel()); 531 } 532 533 /** 534 * Build the ZIP name. 535 * @param contextName The report context name 536 * @return The full ZIP name 537 */ 538 protected String _buildZipName(String contextName) 539 { 540 return FilterNameHelper.filterName(getType() + "-" + getOutputFormat() + "-" + contextName + "-" + _currentFormattedDate) + ".zip"; 541 } 542 543 /** 544 * Object representing a pilotage file 545 * Containing the zip file and the report status 546 */ 547 public static class PilotageFile 548 { 549 private File _zipFile; 550 private PilotageReportStatus _status; 551 552 /** 553 * The pilotage file constructor 554 * @param status the status 555 * @param zipFile the zip file, can be null or non-existent 556 */ 557 public PilotageFile(PilotageReportStatus status, File zipFile) 558 { 559 this._zipFile = zipFile; 560 this._status = status; 561 } 562 563 /** 564 * Get the pilotage report status 565 * @return the pilotage report status 566 */ 567 public PilotageReportStatus getStatus() 568 { 569 return _status; 570 } 571 572 /** 573 * Set the pilotage report status 574 * @param status the status 575 */ 576 public void setStatus(PilotageReportStatus status) 577 { 578 this._status = status; 579 } 580 581 /** 582 * Get the zip file 583 * @return the zip file 584 */ 585 public File getZipFile() 586 { 587 return _zipFile; 588 } 589 590 /** 591 * Set the zip file 592 * @param zipFile the zip file 593 */ 594 public void setZipFile(File zipFile) 595 { 596 this._zipFile = zipFile; 597 } 598 } 599}