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