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