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