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.ModifiableDefaultContent;
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<ModifiableDefaultContent> _internalPopulate(Logger logger)
110    {
111        _updatedContents.clear();
112        
113        List<ModifiableDefaultContent> 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<ModifiableDefaultContent>) 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        _nbNotChangedContents += (int) parameters.getOrDefault("nbNotChangedContents", 0);
132        _nbError += (int) parameters.getOrDefault("nbError", 0);
133
134        return contents;
135    }
136
137    @SuppressWarnings("unchecked")
138    @Override
139    public List<ModifiableDefaultContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception
140    {
141        _updatedContents.clear();
142        
143        File cdmfrFile = new File(_cdmfrFolder, idValue);
144        if (!cdmfrFile.exists())
145        {
146            logger.error("The file '{}' doesn't exists in the repository '{}'.", idValue, _cdmfrFolder.getAbsolutePath());
147        }
148        else if (!cdmfrFile.isFile())
149        {
150            logger.error("The element '{}' is not a file.", cdmfrFile.getAbsolutePath());
151        }
152        else
153        {
154            Map<String, Object> parameters = new HashMap<>();
155            parameters.put("validateAfterImport", validateAfterImport());
156            parameters.put("removalSync", removalSync());
157            parameters.put("contentPrefix", getContentPrefix());
158            parameters.put("collectionId", getId());
159            
160            Map<String, Object> resultMap = _handleFile(cdmfrFile, parameters, logger);
161            
162            _updatedContents.addAll((Set<String>) resultMap.getOrDefault("updatedContents", SetUtils.EMPTY_SET));
163            _nbCreatedContents += (int) resultMap.getOrDefault("nbCreatedContents", 0);
164            _nbSynchronizedContents += (int) resultMap.getOrDefault("nbSynchronizedContents", 0);
165            _nbNotChangedContents += (int) resultMap.getOrDefault("nbNotChangedContents", 0);
166            _nbError += (int) resultMap.getOrDefault("nbError", 0);
167
168            return (List<ModifiableDefaultContent>) resultMap.get("importedPrograms");
169        }
170        
171        return null;
172    }
173
174    /**
175     * Handle the CDM-fr file to import all the programs and its dependencies containing into it.
176     * @param cdmfrFile The CDM-fr file
177     * @param parameters Parameters used to import the file
178     * @param logger The logger
179     * @return The list of imported/synchronized programs
180     */
181    protected Map<String, Object> _handleFile(File cdmfrFile, Map<String, Object> parameters, Logger logger)
182    {
183        String absolutePath = cdmfrFile.getAbsolutePath();
184        
185        logger.info("Processing CDM-fr file '{}'", absolutePath);
186        
187        try (InputStream fis = new FileInputStream(cdmfrFile))
188        {
189            return _importCDMFrComponent.handleInputStream(fis, parameters, logger);
190        }
191        catch (IOException e)
192        {
193            logger.error("An error occured while reading or closing the file {}.", absolutePath, e);
194        }
195        catch (ProcessingException e)
196        {
197            logger.error("An error occured while handling the file {}.", absolutePath, e);
198        }
199        
200        return new HashMap<>();
201    }
202    
203    @Override
204    public Map<String, Map<String, Object>> search(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger)
205    {
206        File[] cdmfrFiles = internalSearch(parameters, logger);
207
208        // Trier
209        cdmfrFiles = _sortFiles(cdmfrFiles, sort);
210        
211        // Parser uniquement les fichiers entre offset et limit
212        int i = 0;
213        Map<String, Map<String, Object>> files = new LinkedHashMap<>();
214        for (File cdmfrFile : cdmfrFiles)
215        {
216            if (i >= offset && i < offset + limit)
217            {
218                Map<String, Object> file = new HashMap<>();
219                file.put(SCC_UNIQUE_ID, cdmfrFile.getName());
220                file.put(_COLUMN_FILENAME, cdmfrFile.getName());
221                file.put(_COLUMN_LAST_MODIFIED, DateUtils.dateToString(new Date(cdmfrFile.lastModified())));
222                file.put(_COLUMN_LENGTH, cdmfrFile.length());
223                files.put(cdmfrFile.getName(), file);
224            }
225            else if (i >= offset + limit)
226            {
227                break;
228            }
229            i++;
230        }
231        
232        return files;
233    }
234
235    @Override
236    public int getTotalCount(Map<String, Object> parameters, Logger logger)
237    {
238        return internalSearch(parameters, logger).length;
239    }
240    
241    @SuppressWarnings("unchecked")
242    private File[] _sortFiles(File[] files, List<Object> sortList)
243    {
244        if (sortList != null)
245        {
246            for (Object sortValueObj : Lists.reverse(sortList))
247            {
248                Map<String, Object> sortValue = (Map<String, Object>) sortValueObj;
249                Comparator<File> comparator = _NAME_TO_COMPARATOR.get(sortValue.get("property"));
250                Object direction = sortValue.get("direction");
251                if (direction != null && direction.toString().equalsIgnoreCase("DESC"))
252                {
253                    comparator = Collections.reverseOrder(comparator);
254                }
255                Arrays.sort(files, comparator);
256            }
257        }
258        
259        return files;
260    }
261    
262    /**
263     * Search values and return the result without any treatment.
264     * @param parameters Search parameters to restrict the search
265     * @param logger The logger
266     * @return {@link File} tab listing the available CDM-fr files corresponding to the filter.
267     */
268    protected File[] internalSearch(Map<String, Object> parameters, Logger logger)
269    {
270        String filename = null;
271        Date lastModifiedAfter = null;
272        Date lastModifiedBefore = null;
273        if (parameters != null && !parameters.isEmpty())
274        {
275            filename = MapUtils.getString(parameters, _CRITERIA_FILENAME);
276            lastModifiedAfter = DateUtils.parse(MapUtils.getString(parameters, _CRITERIA_LAST_MODIFIED_AFTER));
277            lastModifiedBefore = DateUtils.parse(MapUtils.getString(parameters, _CRITERIA_LAST_MODIFIED_BEFORE));
278        }
279        FileFilter filter = new CDMFrFileFilter(filename, lastModifiedAfter, lastModifiedBefore);
280        
281        return _cdmfrFolder.listFiles(filter);
282    }
283
284    @Override
285    protected void configureDataSource(Configuration configuration) throws ConfigurationException
286    {
287        configureSpecificParameters();
288        
289        _odfLang = Config.getInstance().getValue("odf.programs.lang");
290    }
291    
292    /**
293     * Configure the specific parameters of this implementation of CDM-fr import.
294     */
295    protected void configureSpecificParameters()
296    {
297        String cdmfrFolderPath = getParameterValues().get(__PARAM_FOLDER).toString();
298
299        _cdmfrFolder = new File(cdmfrFolderPath);
300        if (!_cdmfrFolder.isAbsolute())
301        {
302            // No : consider it relative to context path
303            _cdmfrFolder = new File(_cocoonContext.getRealPath("/" + cdmfrFolderPath));
304        }
305        
306        if (!_cdmfrFolder.isDirectory())
307        {
308            throw new RuntimeException("The path '" + cdmfrFolderPath + "' defined in the SCC '" + getLabel().getLabel() + "' (" + getId() + ") is not a directory.");
309        }
310        
311        if (!_cdmfrFolder.canRead())
312        {
313            throw new RuntimeException("The folder '" + cdmfrFolderPath + "' defined in the SCC '" + getLabel().getLabel() + "' (" + getId() + ") is not readable.");
314        }
315    }
316    
317    @Override
318    protected void configureSearchModel()
319    {
320        _searchModelConfiguration.displayMaskImported(false);
321        _searchModelConfiguration.addCriterion(_CRITERIA_FILENAME, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_FILENAME"));
322        _searchModelConfiguration.addCriterion(_CRITERIA_LAST_MODIFIED_AFTER, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_LASTMODIFIED_AFTER"), "DATETIME", "edition.date");
323        _searchModelConfiguration.addCriterion(_CRITERIA_LAST_MODIFIED_BEFORE, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_CRITERIA_LASTMODIFIED_BEFORE"), "DATETIME", "edition.date");
324        _searchModelConfiguration.addColumn(_COLUMN_FILENAME, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_FILENAME"), 350);
325        _searchModelConfiguration.addColumn(_COLUMN_LAST_MODIFIED, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_DATE"), 200, true, "DATETIME");
326        _searchModelConfiguration.addColumn(_COLUMN_LENGTH, new I18nizableText("plugin.odf-sync", "PLUGINS_ODF_SYNC_IMPORT_CDMFR_COLUMN_SIZE"), 120, true, "DOUBLE", "Ext.util.Format.fileSize");
327    }
328
329    @Override
330    public void updateSyncInformations(ModifiableDefaultContent content, String syncCode, Logger logger) throws Exception
331    {
332        throw new UnsupportedOperationException("updateSyncInformations() method is not supported for CDMFrSynchronizableContentsCollection.");
333    }
334
335    @Override
336    public void synchronizeContent(ModifiableDefaultContent content, Logger logger) throws Exception
337    {
338        throw new UnsupportedOperationException("synchronizeContent() method is not supported for CDMFrSynchronizableContentsCollection.");
339    }
340
341    @Override
342    public ModifiableDefaultContent getContent(String lang, String idValue)
343    {
344        return null;
345    }
346    
347    @Override
348    public boolean handleRightAssignmentContext()
349    {
350        return false;
351    }
352
353    @Override
354    protected List<Content> _getContentsToRemove(AmetysObjectIterable<ModifiableDefaultContent> contents)
355    {
356        return contents.stream()
357                .filter(content -> !_updatedContents.contains(content.getId()))
358                .collect(Collectors.toList());
359    }
360}