001/*
002 *  Copyright 2016 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.cms.search.solr;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.nio.file.Path;
024import java.time.Duration;
025import java.time.ZoneId;
026import java.time.ZonedDateTime;
027import java.time.format.DateTimeFormatter;
028import java.time.format.FormatStyle;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Optional;
032import java.util.Random;
033
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.components.source.impl.SitemapSource;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.commons.lang3.time.DurationFormatUtils;
039import org.apache.excalibur.source.SourceResolver;
040import org.apache.excalibur.source.SourceUtil;
041import org.quartz.JobDataMap;
042import org.quartz.JobExecutionContext;
043
044import org.ametys.cms.scripts.ReportLocationAction;
045import org.ametys.core.schedule.Schedulable;
046import org.ametys.core.schedule.progression.ContainerProgressionTracker;
047import org.ametys.core.ui.mail.StandardMailBodyHelper;
048import org.ametys.core.util.I18nUtils;
049import org.ametys.core.util.URIUtils;
050import org.ametys.core.util.mail.SendMailHelper;
051import org.ametys.core.util.mail.SendMailHelper.MailBuilder;
052import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
053import org.ametys.plugins.core.schedule.Scheduler;
054import org.ametys.runtime.config.Config;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.i18n.I18nizableTextParameter;
057
058/**
059 * A {@link Schedulable} job for executing scripts.
060 */
061public class SolrExportSchedulable extends AbstractStaticSchedulable
062{
063    /** The key for the export type */
064    public static final String TYPE_KEY = "type";
065    /** The key for the recipient of the report mail */
066    public static final String RECIPIENT_KEY = "recipient";
067    /** The key for the search parameters */
068    public static final String SEARCHPARAMS_KEY = "searchParams";
069    /** The key for the language */
070    public static final String SEARCHPARAMS_LANGUAGE = "lang";
071    /** The key for the export URL */
072    public static final String EXPORT_URL = "exportUrl";
073    
074    private static final String __JOBDATAMAP_TYPE_KEY = Scheduler.PARAM_VALUES_PREFIX + TYPE_KEY;
075    private static final String __JOBDATAMAP_RECIPIENT_KEY = Scheduler.PARAM_VALUES_PREFIX + RECIPIENT_KEY;
076    private static final String __JOBDATAMAP_SEARCHPARAMS_KEY = Scheduler.PARAM_VALUES_PREFIX + SEARCHPARAMS_KEY;
077    private static final String __JOBDATAMAP_LANGUAGE_KEY = Scheduler.PARAM_VALUES_PREFIX + SEARCHPARAMS_LANGUAGE;
078    private static final String __JOBDATAMAP_EXPORT_URL_KEY = Scheduler.PARAM_VALUES_PREFIX + EXPORT_URL;
079
080    /** The avalon source resolver. */
081    protected SourceResolver _sourceResolver;
082    /** I18n Utils */
083    protected I18nUtils _i18nUtils;
084    
085    @Override
086    public void service(ServiceManager manager) throws ServiceException
087    {
088        super.service(manager);
089        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
090        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
091    }
092    
093    @Override
094    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
095    {
096        ZonedDateTime startDate = ZonedDateTime.now();
097        Path reportDirectory = ReportLocationAction.getScriptReportDirectory();
098        File folder = reportDirectory.toFile();
099        
100        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
101        String jsonString = jobDataMap.getString(__JOBDATAMAP_SEARCHPARAMS_KEY);
102        String type = jobDataMap.getString(__JOBDATAMAP_TYPE_KEY);
103        String recipient = jobDataMap.getString(__JOBDATAMAP_RECIPIENT_KEY);
104        String lang = jobDataMap.getString(__JOBDATAMAP_LANGUAGE_KEY);
105        String exportUrl = jobDataMap.getString(__JOBDATAMAP_EXPORT_URL_KEY);
106        String sender = Config.getInstance().getValue("smtp.mail.from");
107        
108        String extension = _getExtension(type);
109        String uri = _getUri(exportUrl);
110        
111        String subject;
112        String html;
113        String text;
114        
115        Locale locale = StringUtils.isNotBlank(lang) ? Locale.forLanguageTag(lang) : Locale.getDefault();
116        I18nizableText undefinedText = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_UNDEFINED_TEXT");
117        I18nizableText start = Optional.ofNullable(startDate)
118                .map(DateTimeFormatter
119                        .ofLocalizedDateTime(FormatStyle.LONG)
120                        .withLocale(locale)
121                        .withZone(ZoneId.systemDefault())
122                        ::format
123                        )
124                .map(I18nizableText::new)
125                .orElse(undefinedText);
126        
127        try
128        {
129            String generatedFile = _generateFile(folder, uri, jsonString, extension);
130            
131            ZonedDateTime endDate = ZonedDateTime.now();
132    
133            I18nizableText duration = Optional.ofNullable(endDate)
134                    .map(end -> Duration.between(startDate, endDate))
135                    .map(Duration::toMillis)
136                    .map(DurationFormatUtils::formatDurationHMS)
137                    .map(I18nizableText::new)
138                    .orElse(undefinedText);
139                
140            
141            subject = _getSubject(type, lang);
142            html = _getHtmlBody(type, generatedFile, extension, start, duration, lang);
143            text = _getTextBody(type, generatedFile, extension, start, duration, lang);
144        }
145        catch (Exception e)
146        {
147            subject = _getErrorSubject(type, lang);
148            html = _getErrorHtmlBody(type, start, lang);
149            text = _getErrorTextBody(type, start, lang);
150        }
151        
152        MailBuilder mailBuilder = SendMailHelper.newMail()
153                      .withSubject(subject)
154                      .withTextBody(text)
155                      .withSender(sender)
156                      .withRecipient(recipient);
157        
158        if (html != null)
159        {
160            mailBuilder.withHTMLBody(html);
161        }
162        
163        mailBuilder.sendMail();
164    }
165    
166    private String _getSubject(String type, String lang)
167    {
168        I18nizableText i18nSubject = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_SUBJECT");
169        return _i18nUtils.translate(i18nSubject, lang);
170    }
171    
172    private String _getURL(String fileName, String extension)
173    {
174        String baseURL = Config.getInstance().getValue("cms.url");
175        return baseURL + "/plugins/cms/async-export/" + fileName + "/export." + extension;
176    }
177    
178    private String _getHtmlBody(String type, String fileName, String extension, I18nizableText start, I18nizableText duration, String lang)
179    {
180        String url = _getURL(fileName, extension);
181        
182        Map<String, I18nizableTextParameter> params = Map.of("url", new I18nizableText(url),
183                "filename", new I18nizableText("export." + extension),
184                "start", start,
185                "duration", duration);
186        
187        try
188        {
189            return StandardMailBodyHelper.newHTMLBody()
190                    .withLanguage(lang)
191                    .withTitle(_getSubject(type, lang))
192                    .withMessage(new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_HTMLBODY", params))
193                    .build();
194        }
195        catch (IOException e)
196        {
197            return null;
198        }
199    }
200    
201    private String _getTextBody(String type, String fileName, String extension, I18nizableText start, I18nizableText duration, String lang)
202    {
203        String url = _getURL(fileName, extension);
204        
205        Map<String, I18nizableTextParameter> params = Map.of("url", new I18nizableText(url),
206                "filename", new I18nizableText("export." + extension),
207                "start", start,
208                "duration", duration);
209        
210        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_TEXTBODY", params);
211        return _i18nUtils.translate(i18nBody, lang);
212    }
213
214    private String _getErrorSubject(String type, String lang)
215    {
216        I18nizableText i18nSubject = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_SUBJECT_ERROR");
217        return _i18nUtils.translate(i18nSubject, lang);
218    }
219
220    private String _getErrorHtmlBody(String type, I18nizableText start, String lang)
221    {
222        Map<String, I18nizableTextParameter> params = Map.of("start", start);
223        
224        try
225        {
226            return StandardMailBodyHelper.newHTMLBody()
227                    .withLanguage(lang)
228                    .withTitle(_getErrorSubject(type, lang))
229                    .withMessage(new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_HTMLBODY_ERROR", params))
230                    .build();
231        }
232        catch (IOException e)
233        {
234            return null;
235        }
236    }
237
238    private String _getErrorTextBody(String type, I18nizableText start, String lang)
239    {
240        Map<String, I18nizableTextParameter> params = Map.of("start", start);
241        
242        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_TEXTBODY_ERROR", params);
243        return _i18nUtils.translate(i18nBody, lang);
244    }
245    
246    private String _getExtension(String type)
247    {
248        return type;
249    }
250    
251    private String _getUri(String exportUrl)
252    {
253        return "cocoon://_plugins/" + exportUrl;
254    }
255    
256    /**
257     * Generate a file from the uri
258     * @param sourceFolder the directory where the file are created
259     * @param uri the uri
260     * @param parameters the parameters of the uri
261     * @param extension the extension of the file
262     * @return output file name (name.extension)
263     * @throws IOException if an error occured with files
264     */
265    protected String _generateFile(File sourceFolder, String uri, String parameters, String extension) throws IOException
266    {
267        SitemapSource source = null;
268        
269        Random rand = new Random();
270        String fileName = String.valueOf(Math.abs(rand.nextInt()));
271        File outputFile = new File(sourceFolder, fileName + "." + extension);
272        while (outputFile.exists())
273        {
274            fileName = String.valueOf(Math.abs(rand.nextInt()));
275            outputFile = new File(sourceFolder, fileName + "." + extension);
276        }
277        
278        try
279        {
280            String fullUri = URIUtils.buildURI(uri, Map.of("parameters", parameters));
281            // Resolve the export to the appropriate pdf url.
282            source = (SitemapSource) _sourceResolver.resolveURI(fullUri, null, null);
283            
284            
285            try (OutputStream pdfTmpOs = new FileOutputStream(outputFile); InputStream sourceIs = source.getInputStream())
286            {
287                SourceUtil.copy(sourceIs, pdfTmpOs);
288            }
289        }
290        finally
291        {
292            if (source != null)
293            {
294                _sourceResolver.release(source);
295            }
296        }
297        return fileName;
298    }
299}