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 javax.mail.MessagingException; 037 038import org.apache.avalon.framework.activity.Initializable; 039import org.apache.avalon.framework.configuration.Configurable; 040import org.apache.avalon.framework.configuration.Configuration; 041import org.apache.avalon.framework.configuration.ConfigurationException; 042import org.apache.avalon.framework.service.ServiceException; 043import org.apache.avalon.framework.service.ServiceManager; 044import org.apache.avalon.framework.service.Serviceable; 045import org.apache.commons.io.FileUtils; 046import org.apache.commons.io.IOUtils; 047import org.apache.commons.lang.StringUtils; 048import org.apache.excalibur.source.Source; 049import org.apache.excalibur.source.SourceResolver; 050 051import org.ametys.cms.FilterNameHelper; 052import org.ametys.core.user.User; 053import org.ametys.core.user.UserIdentity; 054import org.ametys.core.user.UserManager; 055import org.ametys.core.util.I18nUtils; 056import org.ametys.core.util.mail.SendMailHelper; 057import org.ametys.odf.ODFHelper; 058import org.ametys.odf.enumeration.OdfReferenceTableHelper; 059import org.ametys.plugins.odfpilotage.helper.PilotageHelper; 060import org.ametys.plugins.odfpilotage.helper.ReportHelper; 061import org.ametys.plugins.odfpilotage.schedulable.AbstractReportSchedulable; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.runtime.config.Config; 064import org.ametys.runtime.i18n.I18nizableText; 065import org.ametys.runtime.plugin.component.AbstractLogEnabled; 066import org.ametys.runtime.plugin.component.PluginAware; 067 068import com.google.common.collect.ImmutableList; 069import com.google.gson.Gson; 070import com.google.gson.GsonBuilder; 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(_pilotageHelper.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(_pilotageHelper.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.sendMail(subject, null, body, attachments, recipient, _mailFrom); 490 } 491 catch (MessagingException | IOException e) 492 { 493 getLogger().warn("Fail to send email to {}", recipient, e); 494 } 495 } 496 } 497 498 /** 499 * The mail subject. 500 * @return The subject of the mail 501 */ 502 protected String getMailSubject() 503 { 504 return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", "PLUGINS_ODF_PILOTAGE_MAIL_SUBJECT", ImmutableList.of(getReportName()))); 505 } 506 507 /** 508 * The mail body. 509 * @param status the status of the pilotage report 510 * @return The body of the mail 511 */ 512 protected String getMailBody(PilotageReportStatus status) 513 { 514 String key = "PLUGINS_ODF_PILOTAGE_MAIL_BODY_" + status.name(); 515 return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", key, ImmutableList.of(getReportName()))); 516 } 517 518 /** 519 * The report name to add in the mail. 520 * @return The report name 521 */ 522 protected String getReportName() 523 { 524 return _i18nUtils.translate(getLabel()); 525 } 526 527 /** 528 * Build the ZIP name. 529 * @param contextName The report context name 530 * @return The full ZIP name 531 */ 532 protected String _buildZipName(String contextName) 533 { 534 return FilterNameHelper.filterName(getType() + "-" + getOutputFormat() + "-" + contextName + "-" + _currentFormattedDate) + ".zip"; 535 } 536 537 /** 538 * Object representing a pilotage file 539 * Containing the zip file and the report status 540 */ 541 public static class PilotageFile 542 { 543 private File _zipFile; 544 private PilotageReportStatus _status; 545 546 /** 547 * The pilotage file constructor 548 * @param status the status 549 * @param zipFile the zip file, can be null or non-existent 550 */ 551 public PilotageFile(PilotageReportStatus status, File zipFile) 552 { 553 this._zipFile = zipFile; 554 this._status = status; 555 } 556 557 /** 558 * Get the pilotage report status 559 * @return the pilotage report status 560 */ 561 public PilotageReportStatus getStatus() 562 { 563 return _status; 564 } 565 566 /** 567 * Set the pilotage report status 568 * @param status the status 569 */ 570 public void setStatus(PilotageReportStatus status) 571 { 572 this._status = status; 573 } 574 575 /** 576 * Get the zip file 577 * @return the zip file 578 */ 579 public File getZipFile() 580 { 581 return _zipFile; 582 } 583 584 /** 585 * Set the zip file 586 * @param zipFile the zip file 587 */ 588 public void setZipFile(File zipFile) 589 { 590 this._zipFile = zipFile; 591 } 592 } 593}