001/* 002 * Copyright 2021 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.odf.schedulable; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Random; 030 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.cocoon.components.ContextHelper; 034import org.apache.cocoon.components.source.impl.SitemapSource; 035import org.apache.commons.io.FileUtils; 036import org.apache.commons.lang.StringUtils; 037import org.apache.commons.lang3.exception.ExceptionUtils; 038import org.apache.excalibur.source.SourceResolver; 039import org.apache.excalibur.source.SourceUtil; 040import org.quartz.JobDataMap; 041import org.quartz.JobExecutionContext; 042 043import org.ametys.cms.schedule.AbstractSendingMailSchedulable; 044import org.ametys.core.schedule.progression.ContainerProgressionTracker; 045import org.ametys.core.ui.mail.StandardMailBodyHelper; 046import org.ametys.core.util.JSONUtils; 047import org.ametys.odf.ODFHelper; 048import org.ametys.odf.catalog.Catalog; 049import org.ametys.odf.catalog.CatalogsManager; 050import org.ametys.odf.enumeration.OdfReferenceTableHelper; 051import org.ametys.odf.orgunit.OrgUnit; 052import org.ametys.plugins.core.schedule.Scheduler; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.UnknownAmetysObjectException; 055import org.ametys.runtime.config.Config; 056import org.ametys.runtime.i18n.I18nizableText; 057import org.ametys.runtime.model.ElementDefinition; 058import org.ametys.runtime.util.AmetysHomeHelper; 059 060/** 061 * Schedulable to export the ODF catalog as PDF 062 */ 063public class CatalogPDFExportSchedulable extends AbstractSendingMailSchedulable 064{ 065 /** The key for the catalog */ 066 public static final String JOBDATAMAP_CATALOG_KEY = "catalog"; 067 /** The key for the lang */ 068 public static final String JOBDATAMAP_LANG_KEY = "lang"; 069 /** The key for the orgunits */ 070 public static final String JOBDATAMAP_ORGUNIT_KEY = "orgunit"; 071 /** The key for the degrees */ 072 public static final String JOBDATAMAP_DEGREE_KEY = "degree"; 073 /** The key for the query'id */ 074 public static final String JOBDATAMAP_QUERY_KEY = "queryId"; 075 /** The key for the mode */ 076 public static final String JOBDATAMAP_MODE_KEY = "mode"; 077 /** The key for including subprograms */ 078 public static final String JOBDATAMAP_INCLUDE_SUBPROGRAMS = "includeSubPrograms"; 079 /** Mode when catalog is generated from a query */ 080 public static final String MODE_QUERY = "QUERY"; 081 082 /** Map key where the generated filename is stored */ 083 protected static final String _CATALOG_FILENAME = "catalogFilename"; 084 085 /** The Ametys object resolver. */ 086 protected AmetysObjectResolver _resolver; 087 /** The ODF reference table helper. */ 088 protected OdfReferenceTableHelper _odfRefTableHelper; 089 /** The avalon source resolver. */ 090 protected SourceResolver _sourceResolver; 091 092 /** The catalog directory. */ 093 protected File _catalogRootDirectory; 094 /** The JSON utils */ 095 protected JSONUtils _jsonUtils; 096 /** The catalog manager */ 097 protected CatalogsManager _catalogsManager; 098 099 @Override 100 public void service(ServiceManager manager) throws ServiceException 101 { 102 super.service(manager); 103 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 104 _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE); 105 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 106 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 107 _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE); 108 } 109 110 @Override 111 public void initialize() throws Exception 112 { 113 super.initialize(); 114 _catalogRootDirectory = new File(AmetysHomeHelper.getAmetysHomeData(), "odf/catalog"); 115 } 116 117 @Override 118 protected void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 119 { 120 SitemapSource source = null; 121 File pdfTmpFile = null; 122 try 123 { 124 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 125 String catalog = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY); 126 String lang = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_LANG_KEY); 127 128 FileUtils.forceMkdir(_catalogRootDirectory); 129 130 File catalogDir = new File (_catalogRootDirectory, catalog); 131 if (!catalogDir.exists()) 132 { 133 catalogDir.mkdir(); 134 } 135 136 File langDir = new File (catalogDir, lang); 137 if (!langDir.exists()) 138 { 139 langDir.mkdir(); 140 } 141 142 String catalogFilename; 143 144 // Resolve the export to the appropriate pdf url. 145 Map<String, Object> params = new HashMap<>(); 146 147 String mode = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_MODE_KEY); 148 params.put(JOBDATAMAP_MODE_KEY, mode); 149 150 if (MODE_QUERY.equals(mode)) 151 { 152 String queryId = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_QUERY_KEY); 153 154 if (StringUtils.isEmpty(queryId)) 155 { 156 throw new IllegalArgumentException("Id of query is missing to generate PDF catalog from query"); 157 } 158 params.put(JOBDATAMAP_QUERY_KEY, queryId); 159 160 catalogFilename = _getCatalogFilename(queryId, null, null); 161 } 162 else 163 { 164 // Org units 165 Object[] orgunits = _jsonUtils.convertJsonToArray(jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_ORGUNIT_KEY)); 166 if (orgunits.length > 0) 167 { 168 params.put(JOBDATAMAP_ORGUNIT_KEY, orgunits); 169 } 170 171 // Degrees 172 Object[] degrees = _jsonUtils.convertJsonToArray(jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_DEGREE_KEY)); 173 if (degrees.length > 0) 174 { 175 params.put(JOBDATAMAP_DEGREE_KEY, degrees); 176 } 177 178 catalogFilename = _getCatalogFilename(null, orgunits, degrees); 179 } 180 181 // Include subprograms 182 boolean includeSubprograms = jobDataMap.getBoolean(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_INCLUDE_SUBPROGRAMS); 183 params.put(JOBDATAMAP_INCLUDE_SUBPROGRAMS, includeSubprograms); 184 185 // Set the attribute to force the switch to live data 186 ContextHelper.getRequest(_context).setAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL, true); 187 source = (SitemapSource) _sourceResolver.resolveURI("cocoon://_plugins/odf/programs/" + catalog + "/" + lang + "/catalog.pdf", null, params); 188 189 // Save the pdf into a temporary file. 190 String tmpFilename = catalogFilename + "-" + new Random().nextInt() + ".tmp.pdf"; 191 pdfTmpFile = new File(langDir, tmpFilename); 192 193 try ( 194 OutputStream pdfTmpOs = new FileOutputStream(pdfTmpFile); 195 InputStream sourceIs = source.getInputStream() 196 ) 197 { 198 SourceUtil.copy(sourceIs, pdfTmpOs); 199 } 200 201 // If all went well until now, rename the temporary file 202 File catalogFile = new File(langDir, catalogFilename + ".pdf"); 203 if (catalogFile.exists()) 204 { 205 catalogFile.delete(); 206 } 207 208 context.put(_CATALOG_FILENAME, catalogFile.getName()); 209 210 if (!pdfTmpFile.renameTo(catalogFile)) 211 { 212 throw new IOException("Fail to rename catalog.tmp.pdf to catalog.pdf"); 213 } 214 } 215 finally 216 { 217 if (pdfTmpFile != null) 218 { 219 FileUtils.deleteQuietly(pdfTmpFile); 220 } 221 222 if (source != null) 223 { 224 _sourceResolver.release(source); 225 } 226 } 227 228 } 229 230 /** 231 * Get the catalog PDF file name from configuration 232 * @param queryId The id of query to execute. <code>null</code> when export is not based on a query 233 * @param orgunits The restricted orgunits. <code>null</code> when export is based on a query 234 * @param degrees The restricted degrees. <code>null</code> when export is based on a query 235 * @return the computed catalog file name 236 */ 237 protected String _getCatalogFilename(String queryId, Object[] orgunits, Object[] degrees) 238 { 239 List<String> filenamePrefix = new ArrayList<>(); 240 241 filenamePrefix.add("catalog"); 242 243 if (StringUtils.isNotEmpty(queryId)) 244 { 245 filenamePrefix.add(StringUtils.substringAfter(queryId, "query://")); 246 } 247 else 248 { 249 if (orgunits != null && orgunits.length > 0) 250 { 251 Arrays.stream(orgunits) 252 .map(String.class::cast) 253 .map(this::_resolveSilently) 254 .filter(Objects::nonNull) 255 .map(OrgUnit::getUAICode) 256 .filter(StringUtils::isNotEmpty) 257 .forEach(filenamePrefix::add); 258 } 259 260 // Degrees 261 if (degrees != null && degrees.length > 0) 262 { 263 Arrays.stream(degrees) 264 .map(String.class::cast) 265 .map(_odfRefTableHelper::getItemCode) 266 .filter(StringUtils::isNotEmpty) 267 .forEach(filenamePrefix::add); 268 } 269 } 270 return StringUtils.join(filenamePrefix, "-"); 271 } 272 273 private OrgUnit _resolveSilently(String ouId) 274 { 275 try 276 { 277 return _resolver.resolveById(ouId); 278 } 279 catch (UnknownAmetysObjectException e) 280 { 281 getLogger().warn("Can't find orgunit with id {}", ouId); 282 return null; 283 } 284 } 285 286 @Override 287 public Map<String, ElementDefinition> getParameters() 288 { 289 // Remove unsupported widgets if necessary 290 return ODFSchedulableHelper.cleanUnsupportedWidgets(ContextHelper.getRequest(_context), super.getParameters()); 291 } 292 293 @Override 294 protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception 295 { 296 return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT"); 297 } 298 299 @Override 300 protected boolean _isMailBodyInHTML(JobExecutionContext context) throws Exception 301 { 302 return true; 303 } 304 305 @Override 306 protected String _getSuccessMailBody(JobExecutionContext context) throws Exception 307 { 308 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 309 String catalogName = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY); 310 String lang = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_LANG_KEY); 311 312 String catalogTitle = _getCatalogTitle(context); 313 314 String downloadLink = StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"); 315 downloadLink += (downloadLink.endsWith("/") ? "" : "/") + "plugins/odf/download/" + catalogName + "/" + lang + "/" + context.get(_CATALOG_FILENAME); 316 317 try 318 { 319 return StandardMailBodyHelper.newHTMLBody() 320 .withTitle(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT")) 321 .withMessage(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_SUCCESS", List.of(catalogTitle, downloadLink))) 322 .withLink(downloadLink, new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_DOWNLOAD_LINK")) 323 .build(); 324 } 325 catch (IOException e) 326 { 327 getLogger().warn("Failed to build HTML email body for PDF export result. Fallback to no wrapped email", e); 328 return _i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_SUCCESS", List.of(catalogTitle, downloadLink))); 329 } 330 } 331 332 @Override 333 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 334 { 335 return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT"); 336 } 337 338 @Override 339 protected String _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception 340 { 341 try 342 { 343 String catalogTitle = _getCatalogTitle(context); 344 String error = ExceptionUtils.getStackTrace(throwable); 345 346 return StandardMailBodyHelper.newHTMLBody() 347 .withTitle(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT")) 348 .withMessage(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_FAILURE", List.of(catalogTitle))) 349 .withDetails(null, error, true) 350 .build(); 351 } 352 catch (IOException e) 353 { 354 getLogger().warn("Failed to build HTML email body for PDF export result. Fallback to no wrapped email", e); 355 return _i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_FAILURE")); 356 } 357 } 358 359 private String _getCatalogTitle(JobExecutionContext context) 360 { 361 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 362 String catalogName = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY); 363 364 Catalog catalog = _catalogsManager.getCatalog(catalogName); 365 return catalog.getTitle(); 366 } 367}