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}