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