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.core.user.User;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.user.UserManager;
052import org.ametys.core.util.I18nUtils;
053import org.ametys.core.util.mail.SendMailHelper;
054import org.ametys.odf.ODFHelper;
055import org.ametys.odf.enumeration.OdfReferenceTableHelper;
056import org.ametys.plugins.odfpilotage.helper.PilotageHelper;
057import org.ametys.plugins.odfpilotage.helper.ReportHelper;
058import org.ametys.plugins.odfpilotage.schedulable.AbstractReportSchedulable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.repository.jcr.NameHelper;
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 NameHelper.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}