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}