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