001/*
002 *  Copyright 2017 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.plugins.odfsync.cdmfr;
017
018import java.io.File;
019import java.io.FileFilter;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.stream.Collectors;
035
036import org.apache.avalon.framework.configuration.Configuration;
037import org.apache.avalon.framework.configuration.ConfigurationException;
038import org.apache.avalon.framework.context.ContextException;
039import org.apache.avalon.framework.context.Contextualizable;
040import org.apache.cocoon.Constants;
041import org.apache.cocoon.ProcessingException;
042import org.apache.cocoon.environment.Context;
043import org.apache.commons.collections.SetUtils;
044import org.apache.commons.collections4.MapUtils;
045import org.apache.commons.io.comparator.LastModifiedFileComparator;
046import org.apache.commons.io.comparator.NameFileComparator;
047import org.apache.commons.io.comparator.SizeFileComparator;
048import org.slf4j.Logger;
049
050import org.ametys.cms.repository.ModifiableContent;
051import org.ametys.core.schedule.progression.ContainerProgressionTracker;
052import org.ametys.core.schedule.progression.SimpleProgressionTracker;
053import org.ametys.core.util.DateUtils;
054import org.ametys.plugins.repository.AmetysObjectIterable;
055import org.ametys.runtime.config.Config;
056import org.ametys.runtime.i18n.I18nizableText;
057
058import com.google.common.collect.Lists;
059
060/**
061 * Class for CDMFr import and synchronization
062 */
063public class CDMFrSynchronizableContentsCollection extends AbstractCDMFrSynchronizableContentsCollection implements Contextualizable
064{
065    /** Data source parameter : folder */
066    protected static final String __PARAM_FOLDER = "folder";
067    
068    private static final String _CRITERIA_FILENAME = "filename";
069    private static final String _CRITERIA_LAST_MODIFIED_AFTER = "lastModifiedAfter";
070    private static final String _CRITERIA_LAST_MODIFIED_BEFORE = "lastModifiedBefore";
071    private static final String _COLUMN_FILENAME = "filename";
072    private static final String _COLUMN_LAST_MODIFIED = "lastModified";
073    private static final String _COLUMN_LENGTH = "length";
074    
075    private static final Map<String, Comparator<File>> _NAME_TO_COMPARATOR = new HashMap<>();
076    static
077    {
078        _NAME_TO_COMPARATOR.put(_COLUMN_FILENAME, NameFileComparator.NAME_INSENSITIVE_COMPARATOR);
079        _NAME_TO_COMPARATOR.put(_COLUMN_LAST_MODIFIED, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
080        _NAME_TO_COMPARATOR.put(_COLUMN_LENGTH, SizeFileComparator.SIZE_COMPARATOR);
081    }
082
083    /** The Cocoon context */
084    protected Context _cocoonContext;
085    
086    /** CDM-fr folder */
087    protected File _cdmfrFolder;
088
089    /** Default language configured for ODF */
090    protected String _odfLang;
091    
092    /** List of synchronized contents (to avoid a treatment twice or more) */
093    protected Set<String> _updatedContents;
094    
095    @Override
096    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
097    {
098        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
099    }
100
101    @Override
102    public void configure(Configuration configuration) throws ConfigurationException
103    {
104        super.configure(configuration);
105        _updatedContents = new HashSet<>();
106    }
107    
108    @SuppressWarnings("unchecked")
109    @Override
110    protected List<ModifiableContent> _internalPopulate(Logger logger, ContainerProgressionTracker progressionTracker)
111    {
112        _updatedContents.clear();
113        
114        try
115        {
116            _startHandleCDMFR();
117    
118            SimpleProgressionTracker progressionTrackerCDMfrFiles = progressionTracker.addSimpleStep("files", new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_GLOBAL_SYNCHRONIZATION_CDMFR_STEP_LABEL"));
119            
120            List<ModifiableContent> contents = new ArrayList<>();
121            
122            Map<String, Object> parameters = new HashMap<>();
123            parameters.put("validateAfterImport", validateAfterImport());
124            parameters.put("removalSync", removalSync());
125            parameters.put("contentPrefix", getContentPrefix());
126            parameters.put("collectionId", getId());
127            
128            File[] cdmfrFiles = _cdmfrFolder.listFiles(new CDMFrFileFilter(null, null, null));
129            
130            progressionTrackerCDMfrFiles.setSize(cdmfrFiles.length);
131
132            for (File cdmfrFile : cdmfrFiles)
133            {
134                Map<String, Object> resultMap = _handleFile(cdmfrFile, parameters, logger);
135                if (resultMap.containsKey("importedPrograms"))
136                {
137                    contents.addAll((List<ModifiableContent>) resultMap.remove("importedPrograms"));
138                }
139                parameters.putAll(resultMap);
140                progressionTrackerCDMfrFiles.increment();
141            }
142            
143            _updatedContents.addAll((Set<String>) parameters.getOrDefault("updatedContents", SetUtils.EMPTY_SET));
144            _nbCreatedContents += (int) parameters.getOrDefault("nbCreatedContents", 0);
145            _nbSynchronizedContents += (int) parameters.getOrDefault("nbSynchronizedContents", 0);
146            _nbError += (int) parameters.getOrDefault("nbError", 0);
147            
148            return contents;
149        }
150        finally
151        {
152            _endHandleCDMFR(_updatedContents);
153        }
154    }
155    
156    @SuppressWarnings("unchecked")
157    @Override
158    public List<ModifiableContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception
159    {
160        _updatedContents.clear();
161        
162        try
163        {
164            _startHandleCDMFR();
165            
166            File cdmfrFile = new File(_cdmfrFolder, idValue);
167            if (!cdmfrFile.exists())
168            {
169                logger.error("The file '{}' doesn't exists in the repository '{}'.", idValue, _cdmfrFolder.getAbsolutePath());
170            }
171            else if (!cdmfrFile.isFile())
172            {
173                logger.error("The element '{}' is not a file.", cdmfrFile.getAbsolutePath());
174            }
175            else
176            {
177                Map<String, Object> parameters = new HashMap<>();
178                parameters.put("validateAfterImport", validateAfterImport());
179                parameters.put("removalSync", removalSync());
180                parameters.put("contentPrefix", getContentPrefix());
181                parameters.put("collectionId", getId());
182                
183                Map<String, Object> resultMap = _handleFile(cdmfrFile, parameters, logger);
184                
185                _updatedContents.addAll((Set<String>) resultMap.getOrDefault("updatedContents", SetUtils.EMPTY_SET));
186                _nbCreatedContents += (int) resultMap.getOrDefault("nbCreatedContents", 0);
187                _nbSynchronizedContents += (int) resultMap.getOrDefault("nbSynchronizedContents", 0);
188                _nbError += (int) resultMap.getOrDefault("nbError", 0);
189                
190                return (List<ModifiableContent>) resultMap.getOrDefault("importedPrograms", Collections.emptyList());
191            }
192        }
193        finally
194        {
195            _endHandleCDMFR(_updatedContents);
196        }
197        
198        return null;
199    }
200
201    /**
202     * Handle the CDM-fr file to import all the programs and its dependencies containing into it.
203     * @param cdmfrFile The CDM-fr file
204     * @param parameters Parameters used to import the file
205     * @param logger The logger
206     * @return The list of imported/synchronized programs
207     */
208    protected Map<String, Object> _handleFile(File cdmfrFile, Map<String, Object> parameters, Logger logger)
209    {
210        String absolutePath = cdmfrFile.getAbsolutePath();
211        
212        logger.info("Processing CDM-fr file '{}'", absolutePath);
213        
214        try (InputStream fis = new FileInputStream(cdmfrFile))
215        {
216            return _importCDMFrComponent.handleInputStream(fis, parameters, this, logger);
217        }
218        catch (IOException e)
219        {
220            logger.error("An error occured while reading or closing the file {}.", absolutePath, e);
221        }
222        catch (ProcessingException e)
223        {
224            logger.error("An error occured while handling the file {}.", absolutePath, e);
225        }
226        catch (Exception e)
227        {
228            logger.error("An unknown error happens while handling the file {}.", absolutePath, e);
229        }
230        
231        return new HashMap<>();
232    }
233    
234    @Override
235    public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
236    {
237        File[] cdmfrFiles = internalSearch(searchParameters, logger);
238
239        // Trier
240        cdmfrFiles = _sortFiles(cdmfrFiles, sort);
241        
242        // Parser uniquement les fichiers entre offset et limit
243        int i = 0;
244        Map<String, Map<String, Object>> files = new LinkedHashMap<>();
245        for (File cdmfrFile : cdmfrFiles)
246        {
247            if (i >= offset && i < offset + limit)
248            {
249                Map<String, Object> file = new HashMap<>();
250                file.put(SCC_UNIQUE_ID, cdmfrFile.getName());
251                file.put(_COLUMN_FILENAME, cdmfrFile.getName());
252                file.put(_COLUMN_LAST_MODIFIED, DateUtils.dateToString(new Date(cdmfrFile.lastModified())));
253                file.put(_COLUMN_LENGTH, cdmfrFile.length());
254                files.put(cdmfrFile.getName(), file);
255            }
256            else if (i >= offset + limit)
257            {
258                break;
259            }
260            i++;
261        }
262        
263        return files;
264    }
265
266    @Override
267    public int getTotalCount(Map<String, Object> searchParameters, Logger logger)
268    {
269        return internalSearch(searchParameters, logger).length;
270    }
271    
272    @SuppressWarnings("unchecked")
273    private File[] _sortFiles(File[] files, List<Object> sortList)
274    {
275        if (sortList != null)
276        {
277            for (Object sortValueObj : Lists.reverse(sortList))
278            {
279                Map<String, Object> sortValue = (Map<String, Object>) sortValueObj;
280                Comparator<File> comparator = _NAME_TO_COMPARATOR.get(sortValue.get("property"));
281                Object direction = sortValue.get("direction");
282                if (direction != null && direction.toString().equalsIgnoreCase("DESC"))
283                {
284                    comparator = Collections.reverseOrder(comparator);
285                }
286                Arrays.sort(files, comparator);
287            }
288        }
289        
290        return files;
291    }
292    
293    /**
294     * Search values and return the result without any treatment.
295     * @param searchParameters Search parameters to restrict the search
296     * @param logger The logger
297     * @return {@link File} tab listing the available CDM-fr files corresponding to the filter.
298     */
299    protected File[] internalSearch(Map<String, Object> searchParameters, Logger logger)
300    {
301        String filename = null;
302        Date lastModifiedAfter = null;
303        Date lastModifiedBefore = null;
304        if (searchParameters != null && !searchParameters.isEmpty())
305        {
306            filename = MapUtils.getString(searchParameters, _CRITERIA_FILENAME);
307            lastModifiedAfter = DateUtils.parse(MapUtils.getString(searchParameters, _CRITERIA_LAST_MODIFIED_AFTER));
308            lastModifiedBefore = DateUtils.parse(MapUtils.getString(searchParameters, _CRITERIA_LAST_MODIFIED_BEFORE));
309        }
310        FileFilter filter = new CDMFrFileFilter(filename, lastModifiedAfter, lastModifiedBefore);
311        
312        return _cdmfrFolder.listFiles(filter);
313    }
314
315    @Override
316    protected void configureDataSource(Configuration configuration) throws ConfigurationException
317    {
318        configureSpecificParameters();
319        
320        _odfLang = Config.getInstance().getValue("odf.programs.lang");
321    }
322    
323    /**
324     * Configure the specific parameters of this implementation of CDM-fr import.
325     */
326    protected void configureSpecificParameters()
327    {
328        String cdmfrFolderPath = getParameterValues().get(__PARAM_FOLDER).toString();
329
330        _cdmfrFolder = new File(cdmfrFolderPath);
331        if (!_cdmfrFolder.isAbsolute())
332        {
333            // No : consider it relative to context path
334            _cdmfrFolder = new File(_cocoonContext.getRealPath("/" + cdmfrFolderPath));
335        }
336        
337        if (!_cdmfrFolder.isDirectory())
338        {
339            throw new RuntimeException("The path '" + cdmfrFolderPath + "' defined in the SCC '" + getLabel().getLabel() + "' (" + getId() + ") is not a directory.");
340        }
341        
342        if (!_cdmfrFolder.canRead())
343        {
344            throw new RuntimeException("The folder '" + cdmfrFolderPath + "' defined in the SCC '" + getLabel().getLabel() + "' (" + getId() + ") is not readable.");
345        }
346    }
347    
348    @Override
349    protected void configureSearchModel()
350    {
351        _searchModelConfiguration.displayMaskImported(false);
352        _searchModelConfiguration.addCriterion(_CRITERIA_FILENAME, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_FILENAME"));
353        _searchModelConfiguration.addCriterion(_CRITERIA_LAST_MODIFIED_AFTER, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_LASTMODIFIED_AFTER"), "DATETIME", "edition.date");
354        _searchModelConfiguration.addCriterion(_CRITERIA_LAST_MODIFIED_BEFORE, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_LASTMODIFIED_BEFORE"), "DATETIME", "edition.date");
355        _searchModelConfiguration.addColumn(_COLUMN_FILENAME, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_FILENAME"), 350);
356        _searchModelConfiguration.addColumn(_COLUMN_LAST_MODIFIED, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_DATE"), 200, true, "DATETIME");
357        _searchModelConfiguration.addColumn(_COLUMN_LENGTH, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_SIZE"), 120, true, "DOUBLE", "Ext.util.Format.fileSize");
358    }
359
360    @Override
361    public ModifiableContent getContent(String lang, String idValue, boolean forceStrictCheck)
362    {
363        return null;
364    }
365    
366    @Override
367    public boolean handleRightAssignmentContext()
368    {
369        return false;
370    }
371
372    @Override
373    protected List<ModifiableContent> _getContentsToRemove(AmetysObjectIterable<ModifiableContent> contents)
374    {
375        return contents.stream()
376                .filter(content -> !_updatedContents.contains(content.getId()))
377                .collect(Collectors.toList());
378    }
379}