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