001/* 002 * Copyright 2026 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 */ 016 017package org.ametys.odf.init; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.time.Duration; 022import java.time.ZonedDateTime; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.concurrent.atomic.AtomicInteger; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang3.time.DurationFormatUtils; 041import org.apache.excalibur.source.Source; 042import org.apache.excalibur.source.SourceResolver; 043 044import org.ametys.cms.contenttype.ContentType; 045import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 046import org.ametys.plugins.contentio.csv.ImportCSVFileHelper; 047import org.ametys.plugins.contentio.csv.SynchronizeModeEnumerator.ImportMode; 048import org.ametys.runtime.config.Config; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * Helper to synchronize ODF reference table data. 053 */ 054public class OdfRefTableDataHelper extends AbstractLogEnabled implements Component, Contextualizable, Serviceable 055{ 056 /** Avalon Role */ 057 public static final String ROLE = OdfRefTableDataHelper.class.getName(); 058 059 /** The context */ 060 protected Context _context; 061 062 /** The ODF reference table data extension point */ 063 protected OdfRefTableDataExtensionPoint _odfRefTableDataEP; 064 065 /** The import CSV helper */ 066 protected ImportCSVFileHelper _importCSVFileHelper; 067 068 /** The source resolver */ 069 protected SourceResolver _srcResolver; 070 071 /** The content type extension point */ 072 protected ContentTypeExtensionPoint _contentTypeEP; 073 074 public void contextualize(Context context) throws ContextException 075 { 076 _context = context; 077 } 078 079 public void service(ServiceManager manager) throws ServiceException 080 { 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 /** 088 * Import and/or synchronize from {@link OdfRefTableDataExtensionPoint}. 089 * @param importMode The import mode 090 * @return the result of the process 091 */ 092 public OdfRefTableDataResult processRefTableData(ImportMode importMode) 093 { 094 getLogger().info("[BEGIN] ODF reference tables synchronization"); 095 096 OdfRefTableDataResult result = new OdfRefTableDataResult(); 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 106 Map<String, String> dataToImport = _odfRefTableDataEP.getDataToImport(); 107 108 if (getLogger().isInfoEnabled()) 109 { 110 getLogger().info("All CSV files to import: {}", dataToImport.toString()); 111 } 112 113 AtomicInteger count = new AtomicInteger(); 114 Integer total = dataToImport.size(); 115 for (String contentTypeId : dataToImport.keySet()) 116 { 117 getLogger().info("[{}/{}] Synchronizing contents of type {}...", count.incrementAndGet(), total, contentTypeId); 118 119 if (_contentTypeEP.hasExtension(contentTypeId)) 120 { 121 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 122 String dataURI = dataToImport.get(contentTypeId); 123 Source source = null; 124 125 try 126 { 127 source = _srcResolver.resolveURI(dataURI); 128 129 try ( 130 InputStream is = source.getInputStream(); 131 InputStream data = IOUtils.buffer(is); 132 ) 133 { 134 Map<String, Object> csvResult = _importCSVFileHelper.importContents(data, contentType, language, importMode); 135 result.addDetails(contentTypeId, csvResult); 136 137 if (csvResult.containsKey(ImportCSVFileHelper.RESULT_ERROR)) 138 { 139 result.addFail(contentTypeId); 140 getLogger().error("Error while importing ODF reference table data for content type {} with the following reason: '{}'.", contentTypeId, csvResult.get(ImportCSVFileHelper.RESULT_ERROR)); 141 } 142 else 143 { 144 Integer nbImported = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_IMPORTED_COUNT, 0); 145 result.incrementImported(nbImported); 146 Integer nbErrors = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_NB_ERRORS, 0); 147 Integer nbWarnings = (Integer) csvResult.getOrDefault(ImportCSVFileHelper.RESULT_NB_WARNINGS, 0); 148 if (nbErrors > 0 || nbWarnings > 0) 149 { 150 result.addPartial(contentTypeId); 151 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); 152 } 153 else 154 { 155 result.addSuccess(contentTypeId); 156 getLogger().info("Success while importing ODF reference table data for content type {} with {} synchronized entries.", contentTypeId, nbImported); 157 } 158 } 159 } 160 } 161 catch (IOException e) 162 { 163 getLogger().error("Error while importing ODF reference table data of content type {} from file {}.", contentTypeId, dataURI, e); 164 result.addDetails(contentTypeId, Map.of("error", "exception", "message", e.getMessage())); 165 result.addFail(contentTypeId); 166 } 167 finally 168 { 169 if (source != null) 170 { 171 _srcResolver.release(source); 172 } 173 } 174 } 175 else 176 { 177 getLogger().warn("The content type {} is not defined.", contentTypeId); 178 result.addDetails(contentTypeId, Map.of("error", "unexisting")); 179 result.addFail(contentTypeId); 180 } 181 } 182 } 183 catch (Exception e) 184 { 185 getLogger().error("Error during ODF reference tables synchronization", e); 186 throw e; 187 } 188 finally 189 { 190 request.removeAttribute(OdfRefTableDataSynchronizationAccessController.ODF_REF_TABLE_SYNCHRONIZATION); 191 192 if (getLogger().isInfoEnabled()) 193 { 194 getLogger().info(result.resume()); 195 getLogger().info("[END] ODF reference tables synchronization in {}", result.duration()); 196 } 197 } 198 199 return result; 200 } 201 202 /** 203 * Result of the ODF reference table data synchronization. 204 */ 205 public class OdfRefTableDataResult 206 { 207 private Map<String, Map<String, Object>> _details; 208 private List<String> _success; 209 private List<String> _partial; 210 private List<String> _fail; 211 private Integer _imported; 212 private ZonedDateTime _begin; 213 214 /** 215 * Constructor with default values 216 */ 217 protected OdfRefTableDataResult() 218 { 219 _details = new HashMap<>(); 220 _success = new ArrayList<>(); 221 _partial = new ArrayList<>(); 222 _fail = new ArrayList<>(); 223 _imported = 0; 224 _begin = ZonedDateTime.now(); 225 } 226 227 /** 228 * Add the result by content type 229 * @param contentTypeId the content type identifier 230 * @param result the result 231 */ 232 protected void addDetails(String contentTypeId, Map<String, Object> result) 233 { 234 _details.put(contentTypeId, result); 235 } 236 237 /** 238 * Increment the number of imported elements 239 * @param imported number of new elements 240 */ 241 protected void incrementImported(Integer imported) 242 { 243 _imported += imported; 244 } 245 246 /** 247 * Add the item to the success list 248 * @param item the item to add 249 */ 250 protected void addSuccess(String item) 251 { 252 _success.add(item); 253 } 254 255 256 /** 257 * Add the item to the partial list 258 * @param item the item to add 259 */ 260 protected void addPartial(String item) 261 { 262 _partial.add(item); 263 } 264 265 266 /** 267 * Add the item to the fail list 268 * @param item the item to add 269 */ 270 protected void addFail(String item) 271 { 272 _fail.add(item); 273 } 274 275 /** 276 * Get the number of imported elements 277 * @return the number of imported elements 278 */ 279 public Integer getImported() 280 { 281 return _imported; 282 } 283 284 /** 285 * Get the list of successful elements 286 * @return the list of successful elements 287 */ 288 public List<String> getSuccess() 289 { 290 return _success; 291 } 292 293 /** 294 * Get the list of partially successful elements 295 * @return the list of partially successful elements 296 */ 297 public List<String> getPartial() 298 { 299 return _partial; 300 } 301 302 /** 303 * Get the list of failed elements 304 * @return the list of failed elements 305 */ 306 public List<String> getFail() 307 { 308 return _fail; 309 } 310 311 /** 312 * Get the duration with format HH:mm from the object creation 313 * @return the duration 314 */ 315 public String duration() 316 { 317 return Optional.of(ZonedDateTime.now()) 318 .map(end -> Duration.between(_begin, end)) 319 .map(Duration::toMillis) 320 .map(DurationFormatUtils::formatDurationHMS) 321 .orElse("undefined"); 322 } 323 324 /** 325 * Resume the result 326 * @return resume of the result 327 */ 328 public String resume() 329 { 330 StringBuilder resume = new StringBuilder("Resume: "); 331 resume.append(_imported).append(" synchronized entries"); 332 if (!_success.isEmpty()) 333 { 334 resume.append(", ").append(_success.size()).append(" successful reference tables"); 335 } 336 if (!_partial.isEmpty()) 337 { 338 resume.append(", ").append(_partial.size()).append(" partially successful reference tables"); 339 } 340 if (!_fail.isEmpty()) 341 { 342 resume.append(", ").append(_fail.size()).append(" failed reference tables"); 343 } 344 return resume.toString(); 345 } 346 } 347}