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