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}