001/* 002 * Copyright 2022 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.IOException; 019import java.io.InputStream; 020import java.time.Duration; 021import java.time.ZonedDateTime; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.concurrent.atomic.AtomicInteger; 029import java.util.stream.Collectors; 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.environment.Request; 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.lang.StringUtils; 037import org.apache.commons.lang3.exception.ExceptionUtils; 038import org.apache.commons.lang3.time.DurationFormatUtils; 039import org.apache.excalibur.source.Source; 040import org.apache.excalibur.source.SourceResolver; 041import org.quartz.JobDataMap; 042import org.quartz.JobExecutionContext; 043 044import org.ametys.cms.contenttype.ContentType; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.cms.schedule.AbstractSendingMailSchedulable; 047import org.ametys.core.schedule.progression.ContainerProgressionTracker; 048import org.ametys.core.ui.mail.StandardMailBodyHelper; 049import org.ametys.odf.init.OdfRefTableDataExtensionPoint; 050import org.ametys.odf.init.OdfRefTableDataSynchronizationAccessController; 051import org.ametys.plugins.contentio.csv.ImportCSVFileHelper; 052import org.ametys.plugins.contentio.csv.SynchronizeModeEnumerator.ImportMode; 053import org.ametys.plugins.core.schedule.Scheduler; 054import org.ametys.runtime.config.Config; 055import org.ametys.runtime.i18n.I18nizableText; 056 057/** 058 * Import reference table data described by {@link OdfRefTableDataExtensionPoint}. 059 */ 060public class OdfRefTableDataSynchronizationSchedulable extends AbstractSendingMailSchedulable 061{ 062 private static final String __CONTEXT_KEY_SYNC_REPORT = OdfRefTableDataSynchronizationSchedulable.class.getName() + "$syncReport"; 063 064 private static final String __RESULT_IMPORTED_COUNT = "importedCount"; 065 private static final String __RESULT_DETAILS = "details"; 066 private static final String __RESULT_ERROR = "error"; 067 private static final String __RESULT_PARTIAL = "partial"; 068 private static final String __RESULT_SUCCESS = "success"; 069 private static final String __RESULT_DURATION = "duration"; 070 private static final String __IMPORT_MODE = "importMode"; 071 072 private OdfRefTableDataExtensionPoint _odfRefTableDataEP; 073 private ImportCSVFileHelper _importCSVFileHelper; 074 private ContentTypeExtensionPoint _contentTypeEP; 075 private SourceResolver _srcResolver; 076 077 @Override 078 public void service(ServiceManager manager) throws ServiceException 079 { 080 super.service(manager); 081 _odfRefTableDataEP = (OdfRefTableDataExtensionPoint) manager.lookup(OdfRefTableDataExtensionPoint.ROLE); 082 _importCSVFileHelper = (ImportCSVFileHelper) manager.lookup(ImportCSVFileHelper.ROLE); 083 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 084 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 085 } 086 087 @SuppressWarnings("unchecked") 088 @Override 089 protected void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 090 { 091 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 092 ZonedDateTime begin = ZonedDateTime.now(); 093 getLogger().info("[BEGIN] ODF reference tables synchronization"); 094 095 Map<String, Object> result = new HashMap<>(); 096 result.put(__RESULT_IMPORTED_COUNT, 0); 097 098 Request request = ContextHelper.getRequest(_context); 099 100 try 101 { 102 request.setAttribute(OdfRefTableDataSynchronizationAccessController.ODF_REF_TABLE_SYNCHRONIZATION, true); 103 104 String language = Config.getInstance().getValue("odf.programs.lang"); 105 ImportMode importMode = ImportMode.valueOf((String) jobDataMap.get(Scheduler.PARAM_VALUES_PREFIX + __IMPORT_MODE)); 106 107 Map<String, String> dataToImport = _odfRefTableDataEP.getDataToImport(); 108 109 if (getLogger().isInfoEnabled()) 110 { 111 getLogger().info("All CSV files to import: {}", dataToImport.toString()); 112 } 113 114 AtomicInteger count = new AtomicInteger(); 115 Integer total = dataToImport.size(); 116 for (String contentTypeId : dataToImport.keySet()) 117 { 118 getLogger().info("[{}/{}] Synchronizing contents of type {}...", count.incrementAndGet(), total, contentTypeId); 119 120 Map<String, Object> details = (Map<String, Object>) result.computeIfAbsent(__RESULT_DETAILS, __ -> new HashMap<>()); 121 122 if (_contentTypeEP.hasExtension(contentTypeId)) 123 { 124 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 125 String dataURI = dataToImport.get(contentTypeId); 126 Source source = null; 127 128 try 129 { 130 source = _srcResolver.resolveURI(dataURI); 131 132 try ( 133 InputStream is = source.getInputStream(); 134 InputStream data = IOUtils.buffer(is); 135 ) 136 { 137 Map<String, Object> csvResult = _importCSVFileHelper.importContents(data, contentType, language, importMode); 138 details.put(contentTypeId, csvResult); 139 140 if (csvResult.containsKey(ImportCSVFileHelper.RESULT_ERROR)) 141 { 142 _addToListInMap(result, __RESULT_ERROR, contentTypeId); 143 getLogger().error("Error while importing ODF reference table data for content type {} with the following reason: '{}'.", contentTypeId, csvResult.get(ImportCSVFileHelper.RESULT_ERROR)); 144 } 145 else 146 { 147 Integer nbImported = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_IMPORTED_COUNT, 0); 148 result.put(__RESULT_IMPORTED_COUNT, (Integer) result.get(__RESULT_IMPORTED_COUNT) + nbImported); 149 Integer nbErrors = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_NB_ERRORS, 0); 150 Integer nbWarnings = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_NB_WARNINGS, 0); 151 if (nbErrors > 0 || nbWarnings > 0) 152 { 153 _addToListInMap(result, __RESULT_PARTIAL, contentTypeId); 154 getLogger().warn("Some errors and warnings while importing ODF reference table data for content type {} with {} synchronized entries, {} errors and {} warnings.", contentTypeId, nbImported, nbErrors, nbWarnings); 155 } 156 else 157 { 158 _addToListInMap(result, __RESULT_SUCCESS, contentTypeId); 159 getLogger().info("Success while importing ODF reference table data for content type {} with {} synchronized entries.", contentTypeId, nbImported); 160 } 161 } 162 } 163 } 164 catch (IOException e) 165 { 166 getLogger().error("Error while importing ODF reference table data of content type {} from file {}.", contentTypeId, dataURI, e); 167 details.put(contentTypeId, Map.of("error", "exception", "message", e.getMessage())); 168 _addToListInMap(result, __RESULT_ERROR, contentTypeId); 169 } 170 finally 171 { 172 if (source != null) 173 { 174 _srcResolver.release(source); 175 } 176 } 177 } 178 else 179 { 180 getLogger().warn("The content type {} is not defined.", contentTypeId); 181 details.put(contentTypeId, Map.of("error", "unexisting")); 182 _addToListInMap(result, __RESULT_ERROR, contentTypeId); 183 } 184 } 185 } 186 catch (Exception e) 187 { 188 getLogger().error("Error during ODF reference tables synchronization", e); 189 throw e; 190 } 191 finally 192 { 193 request.removeAttribute(OdfRefTableDataSynchronizationAccessController.ODF_REF_TABLE_SYNCHRONIZATION); 194 String duration = Optional.of(ZonedDateTime.now()) 195 .map(end -> Duration.between(begin, end)) 196 .map(Duration::toMillis) 197 .map(DurationFormatUtils::formatDurationHMS) 198 .orElse("undefined"); 199 200 if (getLogger().isInfoEnabled()) 201 { 202 StringBuilder resume = new StringBuilder("Resume: "); 203 resume.append(result.get(__RESULT_IMPORTED_COUNT)); 204 resume.append(" synchronized entries"); 205 if (result.containsKey(__RESULT_SUCCESS)) 206 { 207 resume.append(", "); 208 resume.append(((List<String>) result.get(__RESULT_SUCCESS)).size()); 209 resume.append(" successful reference tables"); 210 } 211 if (result.containsKey(__RESULT_PARTIAL)) 212 { 213 resume.append(", "); 214 resume.append(((List<String>) result.get(__RESULT_PARTIAL)).size()); 215 resume.append(" partial successful reference tables"); 216 } 217 if (result.containsKey(__RESULT_ERROR)) 218 { 219 resume.append(", "); 220 resume.append(((List<String>) result.get(__RESULT_ERROR)).size()); 221 resume.append(" failed reference tables"); 222 } 223 getLogger().info(resume.toString()); 224 getLogger().info("[END] ODF reference tables synchronization in {}", duration); 225 } 226 227 result.put(__RESULT_DURATION, duration); 228 context.put(__CONTEXT_KEY_SYNC_REPORT, result); 229 } 230 } 231 232 @Override 233 protected boolean _isMailBodyInHTML(JobExecutionContext context) throws Exception 234 { 235 return true; 236 } 237 238 @SuppressWarnings("unchecked") 239 @Override 240 protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception 241 { 242 String subjectKey = "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_SUBJECT"; 243 Map<String, Object> result = (Map<String, Object>) context.get(__CONTEXT_KEY_SYNC_REPORT); 244 if (result.containsKey(__RESULT_PARTIAL) || result.containsKey(__RESULT_ERROR)) 245 { 246 subjectKey += "_WITH_ERRORS"; 247 } 248 return new I18nizableText("plugin.odf", subjectKey); 249 } 250 251 @SuppressWarnings("unchecked") 252 @Override 253 protected String _getSuccessMailBody(JobExecutionContext context) throws Exception 254 { 255 try 256 { 257 Map<String, Object> result = (Map<String, Object>) context.get(__CONTEXT_KEY_SYNC_REPORT); 258 259 StringBuilder sb = new StringBuilder(); 260 261 // Duration 262 sb.append(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_DURATION", List.of(result.get(__RESULT_DURATION).toString())))); 263 264 // Imported count 265 sb.append(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_COUNT", List.of(result.get(__RESULT_IMPORTED_COUNT).toString())))); 266 267 // Success 268 if (result.containsKey(__RESULT_SUCCESS)) 269 { 270 sb.append(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_SUCCESS", List.of(String.valueOf(((List<String>) result.get(__RESULT_SUCCESS)).size()))))); 271 } 272 273 // Partial 274 if (result.containsKey(__RESULT_PARTIAL)) 275 { 276 List<String> partialCTypes = (List<String>) result.get(__RESULT_PARTIAL); 277 278 List<String> partialCTypeLabels = partialCTypes.stream() 279 .map(id -> _contentTypeEP.getExtension(id)) 280 .filter(Objects::nonNull) 281 .map(ContentType::getLabel) 282 .map(_i18nUtils::translate) 283 .collect(Collectors.toList()); 284 285 sb.append(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_PARTIAL", List.of(String.valueOf(partialCTypes.size()), StringUtils.join(partialCTypeLabels, ", "))))); 286 } 287 288 // Fail 289 if (result.containsKey(__RESULT_ERROR)) 290 { 291 List<String> errorCTypes = (List<String>) result.get(__RESULT_PARTIAL); 292 293 List<String> errorCTypeLabels = errorCTypes.stream() 294 .map(id -> _contentTypeEP.getExtension(id)) 295 .filter(Objects::nonNull) 296 .map(ContentType::getLabel) 297 .map(_i18nUtils::translate) 298 .collect(Collectors.toList()); 299 300 sb.append(_i18nUtils.translate(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_FAIL", List.of(String.valueOf(errorCTypeLabels.size()), StringUtils.join(errorCTypeLabels, ", "))))); 301 } 302 303 String details = sb.toString(); 304 305 return StandardMailBodyHelper.newHTMLBody() 306 .withTitle(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_SUBJECT")) 307 .addMessage(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME")) 308 .addMessage(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_RESUME_ASK_DETAILS")) 309 .withDetails(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_SUCCESS_BODY_REPORT"), details, false) 310 .build(); 311 } 312 catch (IOException e) 313 { 314 getLogger().error("Failed to build HTML email body for reference table synchronization report", e); 315 return null; 316 } 317 } 318 319 @Override 320 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 321 { 322 return new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_ERROR_SUBJECT"); 323 } 324 325 @Override 326 protected String _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception 327 { 328 String error = StringUtils.join(ExceptionUtils.getStackFrames(throwable), "<br/>"); 329 330 return StandardMailBodyHelper.newHTMLBody() 331 .withTitle(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_ERROR_SUBJECT")) 332 .addMessage(new I18nizableText("plugin.odf", "PLUGINS_ODF_TABLEREF_SYNC_MAIL_ERROR_BODY")) 333 .withDetails(null, error, false) 334 .build(); 335 } 336 337 private void _addToListInMap(Map<String, Object> map, String itemName, String itemToAdd) 338 { 339 @SuppressWarnings("unchecked") 340 List<String> list = (List<String>) map.computeIfAbsent(itemName, __ -> new ArrayList<>()); 341 list.add(itemToAdd); 342 } 343}