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}