001/*
002 *  Copyright 2012 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.odfpilotage.tool;
017
018import java.io.File;
019import java.io.FileFilter;
020import java.io.IOException;
021import java.io.InputStream;
022import java.nio.charset.StandardCharsets;
023import java.time.ZonedDateTime;
024import java.time.format.DateTimeFormatter;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.HashMap;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032import java.util.zip.ZipEntry;
033import java.util.zip.ZipFile;
034
035import org.apache.avalon.framework.parameters.Parameters;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.cocoon.ProcessingException;
039import org.apache.cocoon.acting.ServiceableAction;
040import org.apache.cocoon.environment.ObjectModelHelper;
041import org.apache.cocoon.environment.Redirector;
042import org.apache.cocoon.environment.Request;
043import org.apache.cocoon.environment.SourceResolver;
044import org.apache.commons.collections4.MapUtils;
045import org.apache.commons.io.IOUtils;
046import org.apache.commons.io.comparator.LastModifiedFileComparator;
047import org.apache.commons.io.comparator.NameFileComparator;
048import org.apache.commons.io.comparator.SizeFileComparator;
049import org.apache.commons.lang.StringUtils;
050
051import org.ametys.cms.content.ContentHelper;
052import org.ametys.cms.languages.Language;
053import org.ametys.cms.languages.LanguagesManager;
054import org.ametys.cms.repository.Content;
055import org.ametys.core.cocoon.JSonReader;
056import org.ametys.core.util.DateUtils;
057import org.ametys.core.util.JSONUtils;
058import org.ametys.core.util.ServerCommHelper;
059import org.ametys.odf.catalog.Catalog;
060import org.ametys.odf.catalog.CatalogsManager;
061import org.ametys.odf.program.Program;
062import org.ametys.plugins.odfpilotage.helper.PilotageHelper;
063import org.ametys.plugins.odfpilotage.manager.PilotageLogFileManager;
064import org.ametys.plugins.odfpilotage.report.AbstractPilotageReport;
065import org.ametys.plugins.odfpilotage.report.PilotageReport;
066import org.ametys.plugins.odfpilotage.report.PilotageReport.PilotageReportTarget;
067import org.ametys.plugins.odfpilotage.report.ReportExtensionPoint;
068import org.ametys.plugins.odfpilotage.schedulable.AbstractReportSchedulable;
069import org.ametys.plugins.odfpilotage.schedulable.OrgUnitReportSchedulable;
070import org.ametys.plugins.odfpilotage.schedulable.ProgramReportSchedulable;
071import org.ametys.plugins.repository.AmetysObjectResolver;
072import org.ametys.plugins.repository.UnknownAmetysObjectException;
073
074import com.google.common.collect.Lists;
075import com.google.common.io.PatternFilenameFilter;
076
077/**
078 * SAX the last 30 log files
079 *
080 */
081public class ListReportsAction extends ServiceableAction
082{
083    private static final String _CRITERIA_FILENAME = "filename";
084    private static final String _CRITERIA_LAST_MODIFIED_AFTER = "lastModifiedAfter";
085    private static final String _CRITERIA_LAST_MODIFIED_BEFORE = "lastModifiedBefore";
086
087    private static final String _COLUMN_FILENAME = "reportfile";
088    private static final String _COLUMN_LAST_MODIFIED = "lastModified";
089    private static final String _COLUMN_LENGTH = "length";
090    private static final String _COLUMN_TYPE = "type";
091    private static final String _COLUMN_OUTPUT_FORMAT = "outputFormat";
092    private static final String _COLUMN_CATALOG = "catalog";
093    private static final String _COLUMN_LANG = "lang";
094    private static final String _COLUMN_TARGET = "target";
095    private static final String _COLUMN_CONTEXT = "context";
096    
097    private static final Map<String, Comparator<File>> _NAME_TO_COMPARATOR = new HashMap<>();
098    static
099    {
100        _NAME_TO_COMPARATOR.put(_COLUMN_FILENAME, NameFileComparator.NAME_INSENSITIVE_COMPARATOR);
101        _NAME_TO_COMPARATOR.put(_COLUMN_LAST_MODIFIED, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
102        _NAME_TO_COMPARATOR.put(_COLUMN_LENGTH, SizeFileComparator.SIZE_COMPARATOR);
103    }
104    
105    /** ServerComm Helper */
106    protected ServerCommHelper _serverCommHelper;
107    /** JSON Utils */
108    protected JSONUtils _jsonUtils;
109    /** Pilotage log file manager */
110    protected PilotageLogFileManager _pilotageLogFileManager;
111    /** Pilotage helper */
112    protected PilotageHelper _pilotageHelper;
113    /** Ametys object resolver */
114    protected AmetysObjectResolver _resolver;
115    /** The report extension point */
116    protected ReportExtensionPoint _reportEP;
117    /** The language manager */
118    protected LanguagesManager _languageManager;
119    /** The catalog manager */
120    protected CatalogsManager _catalogManager;
121    /** The content helper */
122    protected ContentHelper _contentHelper;
123    
124    @Override
125    public void service(ServiceManager smanager) throws ServiceException
126    {
127        super.service(smanager);
128        _serverCommHelper = (ServerCommHelper) smanager.lookup(ServerCommHelper.ROLE);
129        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
130        _pilotageLogFileManager = (PilotageLogFileManager) smanager.lookup(PilotageLogFileManager.ROLE);
131        _pilotageHelper = (PilotageHelper) smanager.lookup(PilotageHelper.ROLE);
132        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
133        _reportEP = (ReportExtensionPoint) smanager.lookup(ReportExtensionPoint.ROLE);
134        _languageManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE);
135        _catalogManager = (CatalogsManager) smanager.lookup(CatalogsManager.ROLE);
136        _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE);
137    }
138    
139    @SuppressWarnings("unchecked")
140    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
141    {
142        // Get JS parameters
143        Map<String, Object> jsParameters = _serverCommHelper.getJsParameters();
144        
145        // Search
146        Map<String, Object> searchParams = (Map<String, Object>) jsParameters.get("values");
147        File[] reportFiles = _getReportFiles(searchParams);
148        Integer offset = _getIntValue(jsParameters, "start", 0);
149        Integer limit = _getIntValue(jsParameters, "limit", Integer.MAX_VALUE);
150        List<Map<String, Object>> results = _getReportResults(reportFiles, offset, limit, _getSortList(jsParameters.get("sort")));
151
152        // Construct the JSON object
153        Map<String, Object> objectToRead = new HashMap<>();
154        objectToRead.put("items", results);
155        objectToRead.put("total", reportFiles.length);
156        
157        // Add JSON to the request to be parsed
158        Request request = ObjectModelHelper.getRequest(objectModel);
159        request.setAttribute(JSonReader.OBJECT_TO_READ, objectToRead);
160        
161        return EMPTY_MAP;
162    }
163
164    private int _getIntValue(Map<String, Object> values, String key, int defaultValue)
165    {
166        if (values.containsKey(key))
167        {
168            return Integer.valueOf(values.get(key).toString()).intValue();
169        }
170        
171        return defaultValue;
172    }
173    
174    private File[] _getReportFiles(Map<String, Object> parameters)
175    {
176        String filename = null;
177        ZonedDateTime lastModifiedAfter = null;
178        ZonedDateTime lastModifiedBefore = null;
179        if (parameters != null && !parameters.isEmpty())
180        {
181            filename = MapUtils.getString(parameters, _CRITERIA_FILENAME);
182            lastModifiedAfter = _getZonedDateTimeFromParameters(parameters, _CRITERIA_LAST_MODIFIED_AFTER);
183            lastModifiedBefore = _getZonedDateTimeFromParameters(parameters, _CRITERIA_LAST_MODIFIED_BEFORE);
184        }
185        FileFilter filter = new PilotageFileFilter(filename, lastModifiedAfter, lastModifiedBefore);
186
187        return _pilotageHelper.getPilotageFolder().listFiles(filter);
188    }
189    
190    private ZonedDateTime _getZonedDateTimeFromParameters(Map<String, Object> parameters, String parameterName)
191    {
192        String dateAsString = MapUtils.getString(parameters, parameterName);
193        return StringUtils.isNotEmpty(dateAsString) ? ZonedDateTime.parse(dateAsString, DateTimeFormatter.ISO_DATE_TIME) : null;
194    }
195    
196    private List<Map<String, Object>> _getReportResults(File[] reportFiles, Integer offset, Integer limit, List<Object> sort) throws ProcessingException
197    {
198        List<Map<String, Object>> reports = new LinkedList<>();
199        
200        int count = 0;
201
202        for (File reportFile : _sortFiles(reportFiles, sort))
203        {
204            if (count >= offset && count < offset + limit)
205            {
206                String filename = reportFile.getName();
207                
208                Map<String, Object> report = new HashMap<>();
209                report.put(_COLUMN_FILENAME, filename);
210                report.put(_COLUMN_LENGTH, String.valueOf(reportFile.length()));
211                report.put(_COLUMN_LAST_MODIFIED, DateUtils.epochMilliToString(reportFile.lastModified()));
212                
213                // Parse manifest.json (if it exists)
214                report.put("properties", _parseManifest(reportFile));
215                
216                File[] logFiles = _getLogsFiles(filename);
217                
218                if (logFiles != null && logFiles.length > 0)
219                {
220                    if (logFiles.length == 1)
221                    {
222                        File logFile = logFiles[0];
223                        report.put("logfile", logFile.getName());
224                    }
225                    else
226                    {
227                        throw new ProcessingException("Found more than one logfile with name '" + logFiles[0].getName() + ".log'.");
228                    }
229                }
230
231                reports.add(report);
232            }
233            else if (count >= offset + limit)
234            {
235                break;
236            }
237            count++;
238        }
239        
240        return reports;
241    }
242    
243    private Map<String, Object> _parseManifest(File reportFile)
244    {
245        try (ZipFile zipFile = new ZipFile(reportFile);)
246        {
247            ZipEntry zipEntry = zipFile.getEntry(AbstractPilotageReport.MANIFEST_FILENAME);
248            if (zipEntry != null)
249            {
250                try (InputStream is = zipFile.getInputStream(zipEntry);)
251                {
252                    String manifestContent = IOUtils.toString(is, StandardCharsets.UTF_8);
253                    Map<String, Object> manifestMap = _jsonUtils.convertJsonToMap(manifestContent);
254                    
255                    Map<String, Object> infos = new HashMap<>();
256                    infos.put(_COLUMN_TYPE, _getReportTypeInfos((String) manifestMap.get("type")));
257                    
258                    infos.put(_COLUMN_OUTPUT_FORMAT, manifestMap.get(AbstractReportSchedulable.JOBDATAMAP_OUTPUT_FORMAT_KEY));
259                    String target = manifestMap.get("target").toString().toUpperCase();
260                    infos.put(_COLUMN_TARGET, target);
261                    if (target.equals(PilotageReportTarget.ORGUNIT.name()))
262                    {
263                        String contextId = (String) manifestMap.get(OrgUnitReportSchedulable.JOBDATAMAP_ORGUNIT_KEY);
264                        if (StringUtils.isNotEmpty(contextId))
265                        {
266                            infos.put(_COLUMN_CONTEXT, _getContentInfos((String) manifestMap.get(OrgUnitReportSchedulable.JOBDATAMAP_ORGUNIT_KEY)));
267                        }
268                        infos.put(_COLUMN_CATALOG, _getCatalogInfos((String) manifestMap.get(OrgUnitReportSchedulable.JOBDATAMAP_CATALOG_KEY)));
269                        infos.put(_COLUMN_LANG, _getLanguageInfos((String) manifestMap.get(OrgUnitReportSchedulable.JOBDATAMAP_LANG_KEY)));
270                    }
271                    else if (target.equals(PilotageReportTarget.PROGRAM.name()))
272                    {
273                        String programId = manifestMap.get(ProgramReportSchedulable.JOBDATAMAP_PROGRAM_KEY).toString();
274                        infos.put(_COLUMN_CONTEXT, _getContentInfos(programId));
275                        try
276                        {
277                            Program program = _resolver.resolveById(programId);
278                            infos.put(_COLUMN_CATALOG, _getCatalogInfos(program.getCatalog()));
279                            infos.put(_COLUMN_LANG, _getLanguageInfos(program.getLanguage()));
280                        }
281                        catch (UnknownAmetysObjectException e)
282                        {
283                            getLogger().warn("The content '" + programId + "' has probably been deleted.");
284                        }
285                    }
286                    return infos;
287                }
288            }
289        }
290        catch (IOException e)
291        {
292            getLogger().error("An error occured while parsing the manifest.json file of " + reportFile.getName(), e);
293        }
294        
295        return Collections.EMPTY_MAP;
296    }
297    
298    private Map<String, Object> _getReportTypeInfos(String typeId)
299    {
300        Map<String, Object> infos = new HashMap<>();
301        infos.put("value", typeId);
302        
303        PilotageReport report = _reportEP.getExtension(typeId);
304        if (report != null)
305        {
306            infos.put("label", report.getLabel());
307        }
308        
309        return infos;
310    }
311    
312    private Map<String, Object> _getContentInfos(String contentId)
313    {
314        Map<String, Object> infos = new HashMap<>();
315        
316        infos.put("id", contentId);
317        
318        try
319        {
320            Content content = _resolver.resolveById(contentId);
321            infos.put("title", content.getTitle());
322            infos.put("isSimple", _contentHelper.isSimple(content));
323        }
324        catch (UnknownAmetysObjectException e)
325        {
326            // Nothing
327        }
328        
329        return infos;
330    }
331    
332    private Map<String, Object> _getCatalogInfos(String name)
333    {
334        Map<String, Object> infos = new HashMap<>();
335        infos.put("value", name);
336        
337        Catalog catalog = _catalogManager.getCatalog(name);
338        if (catalog != null)
339        {
340            infos.put("label", catalog.getTitle());
341        }
342        return infos;
343        
344    }
345    
346    private Map<String, Object> _getLanguageInfos(String lang)
347    {
348        Map<String, Object> infos = new HashMap<>();
349        infos.put("code", lang);
350        
351        if (lang != null)
352        {
353            Language language = _languageManager.getLanguage(lang);
354            if (language != null)
355            {
356                infos.put("icon", language.getSmallIcon());
357                infos.put("label", language.getLabel());
358            }
359        }
360        
361        return infos;
362    }
363    /**
364     * Get the logs files
365     * @param filename Name of the file to retrieve a log 
366     * @return the logs files
367     */
368    private File[] _getLogsFiles(String filename)
369    {
370        File logDir = _pilotageLogFileManager.getLogsDirectory();
371
372        int filenameLength = filename.lastIndexOf(".");
373        String logFileName = filename.substring(0, filename.indexOf("-")) + filename.substring(filenameLength - 11, filenameLength)  + ".log";
374        return logDir.listFiles(new PatternFilenameFilter(logFileName));
375    }
376
377    private List<Object> _getSortList(Object sortValues)
378    {
379        if (sortValues != null)
380        {
381            return _jsonUtils.convertJsonToList(sortValues.toString());
382        }
383        
384        return null;
385    }
386    
387    @SuppressWarnings("unchecked")
388    private File[] _sortFiles(File[] files, List<Object> sortList)
389    {
390        if (sortList != null)
391        {
392            for (Object sortValueObj : Lists.reverse(sortList))
393            {
394                Map<String, Object> sortValue = (Map<String, Object>) sortValueObj;
395                Comparator<File> comparator = _NAME_TO_COMPARATOR.get(sortValue.get("property"));
396                Object direction = sortValue.get("direction");
397                if (direction != null && direction.toString().equalsIgnoreCase("DESC"))
398                {
399                    comparator = Collections.reverseOrder(comparator);
400                }
401                Arrays.sort(files, comparator);
402            }
403        }
404        
405        return files;
406    }
407}