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        SendMailHelper.sendMail(subject, html, text, recipient, sender);
149    }
150    
151    private String _getSubject(String type, String lang)
152    {
153        I18nizableText i18nSubject = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_SUBJECT");
154        return _i18nUtils.translate(i18nSubject, lang);
155    }
156    
157    private String _getURL(String fileName, String extension)
158    {
159        String baseURL = Config.getInstance().getValue("cms.url");
160        return baseURL + "/plugins/cms/async-export/" + fileName + "/export." + extension;
161    }
162    
163    private String _getHtmlBody(String type, String fileName, String extension, I18nizableText start, I18nizableText duration, String lang)
164    {
165        String url = _getURL(fileName, extension);
166        
167        Map<String, I18nizableTextParameter> params = Map.of("url", new I18nizableText(url),
168                "filename", new I18nizableText("export." + extension),
169                "start", start,
170                "duration", duration);
171        
172        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_HTMLBODY", params);
173        return _i18nUtils.translate(i18nBody, lang);
174    }
175    
176    private String _getTextBody(String type, String fileName, String extension, I18nizableText start, I18nizableText duration, String lang)
177    {
178        String url = _getURL(fileName, extension);
179        
180        Map<String, I18nizableTextParameter> params = Map.of("url", new I18nizableText(url),
181                "filename", new I18nizableText("export." + extension),
182                "start", start,
183                "duration", duration);
184        
185        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_TEXTBODY", params);
186        return _i18nUtils.translate(i18nBody, lang);
187    }
188
189    private String _getErrorSubject(String type, String lang)
190    {
191        I18nizableText i18nSubject = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_SUBJECT_ERROR");
192        return _i18nUtils.translate(i18nSubject, lang);
193    }
194
195    private String _getErrorHtmlBody(String type, I18nizableText start, String lang)
196    {
197        Map<String, I18nizableTextParameter> params = Map.of("start", start);
198        
199        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_HTMLBODY_ERROR", params);
200        return _i18nUtils.translate(i18nBody, lang);
201    }
202
203    private String _getErrorTextBody(String type, I18nizableText start, String lang)
204    {
205        Map<String, I18nizableTextParameter> params = Map.of("start", start);
206        
207        I18nizableText i18nBody = new I18nizableText("plugin.cms", "UITOOL_SEARCH_ASYNC_EXPORT_" + type.toUpperCase() + "_MAIL_TEXTBODY_ERROR", params);
208        return _i18nUtils.translate(i18nBody, lang);
209    }
210    
211    private String _getExtension(String type)
212    {
213        return type;
214    }
215    
216    private String _getUri(String exportUrl)
217    {
218        return "cocoon://_plugins/cms/" + exportUrl;
219    }
220    
221    /**
222     * Generate a file from the uri
223     * @param sourceFolder the directory where the file are created
224     * @param uri the uri
225     * @param parameters the parameters of the uri
226     * @param extension the extension of the file
227     * @return output file name (name.extension)
228     * @throws IOException if an error occured with files
229     */
230    protected String _generateFile(File sourceFolder, String uri, String parameters, String extension) throws IOException
231    {
232        SitemapSource source = null;
233        
234        Random rand = new Random();
235        String fileName = String.valueOf(Math.abs(rand.nextInt()));
236        File outputFile = new File(sourceFolder, fileName + "." + extension);
237        while (outputFile.exists())
238        {
239            fileName = String.valueOf(Math.abs(rand.nextInt()));
240            outputFile = new File(sourceFolder, fileName + "." + extension);
241        }
242        
243        try
244        {
245            String fullUri = URIUtils.buildURI(uri, Map.of("parameters", parameters));
246            // Resolve the export to the appropriate pdf url.
247            source = (SitemapSource) _sourceResolver.resolveURI(fullUri, null, null);
248            
249            
250            try (OutputStream pdfTmpOs = new FileOutputStream(outputFile); InputStream sourceIs = source.getInputStream())
251            {
252                SourceUtil.copy(sourceIs, pdfTmpOs);
253            }
254        }
255        finally
256        {
257            if (source != null)
258            {
259                _sourceResolver.release(source);
260            }
261        }
262        return fileName;
263    }
264}