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