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.time.LocalDate;
025import java.time.format.DateTimeFormatter;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.zip.ZipEntry;
031import java.util.zip.ZipOutputStream;
032
033import javax.mail.MessagingException;
034
035import org.apache.avalon.framework.activity.Initializable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.commons.io.FileUtils;
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.lang.StringUtils;
042import org.apache.excalibur.source.Source;
043import org.apache.excalibur.source.SourceResolver;
044
045import org.ametys.cms.FilterNameHelper;
046import org.ametys.core.user.User;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.user.UserManager;
049import org.ametys.core.util.I18nUtils;
050import org.ametys.core.util.mail.SendMailHelper;
051import org.ametys.odf.ODFHelper;
052import org.ametys.odf.enumeration.OdfReferenceTableHelper;
053import org.ametys.plugins.odfpilotage.helper.PilotageHelper;
054import org.ametys.plugins.odfpilotage.helper.ReportHelper;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.runtime.config.Config;
057import org.ametys.runtime.i18n.I18nizableText;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059import org.ametys.runtime.plugin.component.PluginAware;
060
061import com.google.common.collect.ImmutableList;
062
063/**
064 * The abstract class for pilotage reports.
065 */
066public abstract class AbstractPilotageReport extends AbstractLogEnabled implements PilotageReport, Serviceable, Initializable, PluginAware
067{
068    /**
069     * The enumerator for different pilotage report status
070     */
071    public enum PilotageReportStatus
072    {
073        /** If the report has failed */
074        FAIL,
075        /** If the report is a success */
076        SUCCESS,
077        /** If there are no file in the report */
078        NO_FILE
079    }
080    
081    /** The source resolver */
082    protected SourceResolver _sourceResolver;
083    
084    /** The pilotage helper */
085    protected PilotageHelper _pilotageHelper;
086    
087    /** The ametys object resolver */
088    protected AmetysObjectResolver _resolver;
089    
090    /** The report helper */
091    protected ReportHelper _reportHelper;
092    
093    /** The ODF enumeration helper */
094    protected OdfReferenceTableHelper _refTableHelper;
095    
096    /** The ODF helper */
097    protected ODFHelper _odfHelper;
098    
099    /** The user manager */
100    protected UserManager _userManager;
101    
102    /** The I18N utils */
103    protected I18nUtils _i18nUtils;
104    
105    /** The tmp folder */
106    protected File _tmpFolder;
107    
108    /** The current date formatted to yyyy-MM-dd */
109    protected String _currentFormattedDate;
110
111    private String _pluginName;
112    private String _mailFrom;
113    
114    @Override
115    public void initialize() throws Exception
116    {
117        _tmpFolder = new File(_pilotageHelper.getTmpPilotageFolder(), getType());
118        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
119    }
120    
121    @Override
122    public void setPluginInfo(String pluginName, String featureName, String id)
123    {
124        _pluginName = pluginName;
125    }
126    
127    @Override
128    public void service(ServiceManager manager) throws ServiceException
129    {
130        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
131        _pilotageHelper = (PilotageHelper) manager.lookup(PilotageHelper.ROLE);
132        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
133        _reportHelper = (ReportHelper) manager.lookup(ReportHelper.ROLE);
134        _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
135        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
136        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
137        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
138    }
139
140    /**
141     * Launch a report generation on an orgunit.
142     * @param reportParameters The report parameters
143     * @return the name of the generated file
144     * @throws Exception if an exception occurs
145     */
146    protected abstract String launchByOrgUnit(Map<String, String> reportParameters) throws Exception;
147
148    /**
149     * Launch a report generation on a program.
150     * @param reportParameters The report parameters
151     * @return the name of the generated file
152     * @throws Exception if an exception occurs
153     */
154    protected abstract String launchByProgram(Map<String, String> reportParameters) throws Exception;
155    
156    /**
157     * Get the name of the report
158     * @return The report name
159     */
160    protected abstract String getType();
161    
162    /**
163     * Get the plugin name to build the pipeline.
164     * @return The plugin name
165     */
166    protected String getPluginName()
167    {
168        return _pluginName;
169    }
170
171    /**
172     * Build the pipeline to launch the transformation.
173     * @param outputFolderName The name of the output folder name
174     * @return The pipeline for transformation
175     */
176    protected String getPipeline(String outputFolderName)
177    {
178        return "report/" + getType() + "/" + outputFolderName;
179    }
180    
181    @Override
182    public synchronized void launch(PilotageReportTarget target, Map<String, String> reportParameters, UserIdentity user)
183    {
184        getLogger().info("Début du rapport de pilotage");
185        long time_0 = System.currentTimeMillis();
186
187        String contextName = null;
188        try
189        {
190            FileUtils.deleteQuietly(_tmpFolder);
191            _tmpFolder.mkdir();
192            _currentFormattedDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
193
194            switch (target)
195            {
196                case PROGRAM:
197                    contextName = launchByProgram(reportParameters);
198                    break;
199                case ORGUNIT:
200                    contextName = launchByOrgUnit(reportParameters);
201                    break;
202                default:
203                    break;
204            }
205        }
206        catch (Exception e)
207        {
208            getLogger().error("Erreur d'écriture du rapport.", e);
209        }
210        finally
211        {
212            PilotageFile pilotageFile = new PilotageFile(PilotageReportStatus.FAIL, null);
213            try
214            {
215                pilotageFile = createZipFile(_tmpFolder, contextName);
216            }
217            catch (Exception e)
218            {
219                getLogger().error("Une erreur est survenue lors de la compression des rapports.", e);
220            }
221            finally
222            {
223                sendMail(pilotageFile, user);
224                
225                FileUtils.deleteQuietly(_tmpFolder);
226                _currentFormattedDate = null;
227                
228                long time_1 = System.currentTimeMillis();
229                getLogger().info("Calcul et écriture du rapport de pilotage effectué en {} ms.", time_1 - time_0);
230            }
231        }
232    }
233
234    /**
235     * Convert the report from XML to the required format
236     * @param outputFolder folder where the file will stay temporarily
237     * @param fileName the name of the file
238     * @param xmlFile the file to be converted
239     * @throws IOException if an error occurs
240     */
241    protected void convertReport(File outputFolder, String fileName, File xmlFile) throws IOException
242    {
243        Source source = null;
244        try
245        {
246            // Transform XML to HTML-XLS
247            source = _sourceResolver.resolveURI("cocoon://_plugins/" + getPluginName() + "/" + getPipeline(outputFolder.getName()) + "/" + fileName);
248            try (InputStream is = source.getInputStream())
249            {
250                // Delete existing file
251                File file = new File(outputFolder, fileName);
252                FileUtils.deleteQuietly(file);
253                file.createNewFile();
254                
255                // Save file
256                try (OutputStream os = new FileOutputStream(file))
257                {
258                    IOUtils.copy(is, os);
259                }
260            }
261        }
262        finally
263        {
264            FileUtils.deleteQuietly(xmlFile);
265            if (source != null)
266            {
267                _sourceResolver.release(source);
268            }
269        }
270    }
271    
272    /**
273     * Compress a folder to zip format 
274     * @param folderToZip the folder to be compressed
275     * @param contextName the name of the report context
276     * @return The pilotage file
277     * @throws IOException if an exception occurs
278     */
279    protected PilotageFile createZipFile(File folderToZip, String contextName) throws IOException
280    {
281        if (StringUtils.isEmpty(contextName))
282        {
283            return new PilotageFile(PilotageReportStatus.FAIL, null);
284        }
285        
286        File zipFile = new File(_pilotageHelper.getPilotageFolder(), _buildZipName(contextName));
287
288        FileUtils.deleteQuietly(zipFile);
289        File[] files = folderToZip.listFiles();
290
291        if (files.length == 0)
292        {
293            getLogger().warn("Aucun fichier généré");
294            return new PilotageFile(PilotageReportStatus.NO_FILE, null);
295        }
296
297        getLogger().info("Création de l'archive");
298        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile)))
299        {
300            for (File file : files)
301            {
302                try (FileInputStream is = new FileInputStream(file))
303                {
304                    byte[] buf = new byte[1024];
305                    int len;
306                    zos.putNextEntry(new ZipEntry(file.getName()));
307                    while ((len = is.read(buf)) > 0)
308                    {
309                        zos.write(buf, 0, len);
310                    }
311                }
312                finally
313                {
314                    zos.closeEntry();
315                }
316            }
317        }
318        
319        return new PilotageFile(PilotageReportStatus.SUCCESS, zipFile);
320    }
321    
322    /**
323     * Send a mail with the ZIP file as attachment at the end of the report generation.
324     * @param file the pilotage file
325     * @param user the recipient if he has an email
326     */
327    protected void sendMail(PilotageFile file, UserIdentity user)
328    {
329        String recipient = Optional.ofNullable(user)
330            .map(_userManager::getUser)
331            .map(User::getEmail)
332            .filter(StringUtils::isNotEmpty)
333            .orElse(null);
334        
335        if (recipient != null)
336        {
337            String subject = getMailSubject();
338            String body = getMailBody(file.getStatus());
339            try
340            {
341                File zipFile = file.getZipFile();
342                List<File> attachments = new ArrayList<>();
343                if (zipFile != null && zipFile.exists())
344                {
345                    attachments.add(zipFile);
346                }
347                
348                getLogger().info("Envoi du rapport par mail");
349                SendMailHelper.sendMail(subject, null, body, attachments, recipient, _mailFrom);
350            }
351            catch (MessagingException | IOException e)
352            {
353                getLogger().warn("Fail to send email to {}", recipient, e); 
354            }
355        }
356    }
357
358    /**
359     * The mail subject.
360     * @return The subject of the mail
361     */
362    protected String getMailSubject()
363    {
364        return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", "PLUGINS_ODF_PILOTAGE_MAIL_SUBJECT", ImmutableList.of(getReportName())));
365    }
366
367    /**
368     * The mail body.
369     * @param status the status of the pilotage report
370     * @return The body of the mail
371     */
372    protected String getMailBody(PilotageReportStatus status)
373    {
374        String key = "PLUGINS_ODF_PILOTAGE_MAIL_BODY_" + status.name();
375        return _i18nUtils.translate(new I18nizableText("plugin.odf-pilotage", key, ImmutableList.of(getReportName())));
376    }
377    
378    /**
379     * The report name to add in the mail.
380     * @return The report name
381     */
382    protected abstract String getReportName();
383
384    /**
385     * Build the ZIP name.
386     * @param contextName The report context name
387     * @return The full ZIP name
388     */
389    private String _buildZipName(String contextName)
390    {
391        return FilterNameHelper.filterName(getType() + "-" + contextName + "-" + _currentFormattedDate) + ".zip";
392    }
393    
394    /**
395     * Object representing a pilotage file
396     * Containing the zip file and the report status
397     */
398    public static class PilotageFile
399    {
400        private File _zipFile;
401        private PilotageReportStatus _status;
402        
403        /**
404         * The pilotage file constructor
405         * @param status the status
406         * @param zipFile the zip file, can be null or non-existent
407         */
408        public PilotageFile(PilotageReportStatus status, File zipFile)
409        {
410            this._zipFile = zipFile;
411            this._status = status;
412        }
413        
414        /**
415         * Get the pilotage report status
416         * @return the pilotage report status
417         */
418        public PilotageReportStatus getStatus()
419        {
420            return _status;
421        }
422        
423        /**
424         * Set the pilotage report status
425         * @param status the status
426         */
427        public void setStatus(PilotageReportStatus status)
428        {
429            this._status = status;
430        }
431        
432        /**
433         * Get the zip file
434         * @return the zip file
435         */
436        public File getZipFile()
437        {
438            return _zipFile;
439        }
440        
441        /**
442         * Set the zip file
443         * @param zipFile the zip file
444         */
445        public void setZipFile(File zipFile)
446        {
447            this._zipFile = zipFile;
448        }
449    }
450}