001/*
002 *  Copyright 2020 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.contentio.archive;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.Map;
023import java.util.Optional;
024import java.util.function.Predicate;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.quartz.JobDataMap;
030import org.quartz.JobExecutionContext;
031import org.quartz.JobExecutionException;
032
033import org.ametys.core.schedule.progression.ContainerProgressionTracker;
034import org.ametys.plugins.contentio.archive.ImportReport.ImportError;
035import org.ametys.plugins.core.schedule.Scheduler;
036import org.ametys.runtime.i18n.I18nizableText;
037import org.ametys.runtime.i18n.I18nizableTextParameter;
038
039/**
040 * Job for importing data from an archive.
041 */
042public class ImportArchiveSchedulable extends AbstractArchiveSchedulable
043{
044    /** This schedulable id */
045    public static final String ID = ImportArchiveSchedulable.class.getName();
046    
047    static final String ARCHIVE_KEY = "archive";
048    static final String ELEMENTS_KEY = "elements";
049    static final String MERGE_POLICY_KEY = "mergePolicy";
050    
051    private static final String __JOBDATAMAP_ARCHIVE_KEY = Scheduler.PARAM_VALUES_PREFIX + ARCHIVE_KEY;
052    private static final String __JOBDATAMAP_ELEMENTS_KEY = Scheduler.PARAM_VALUES_PREFIX + ELEMENTS_KEY;
053    private static final String __JOBDATAMAP_MERGE_POLICY_KEY = Scheduler.PARAM_VALUES_PREFIX + MERGE_POLICY_KEY;
054    
055    private static final String __ERROR_TEMPLATE = "<li><pre>%s \n %s</pre></li>";
056    
057    private ArchiverExtensionPoint _archiverEP;
058    
059    @Override
060    public void service(ServiceManager manager) throws ServiceException
061    {
062        super.service(manager);
063        _archiverEP = (ArchiverExtensionPoint) manager.lookup(ArchiverExtensionPoint.ROLE);
064    }
065    
066    @Override
067    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
068    {
069        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
070        String archiveName = jobDataMap.getString(__JOBDATAMAP_ARCHIVE_KEY);
071        Optional<Collection<String>> elements = Optional.of(__JOBDATAMAP_ELEMENTS_KEY)
072                .map(jobDataMap::get)
073                .filter(Collection.class::isInstance)
074                .map(Collection.class::cast);
075        MergePolicy mergePolicy = (MergePolicy) jobDataMap.get(__JOBDATAMAP_MERGE_POLICY_KEY);
076        Merger merger = mergePolicy.getMerger();
077        
078        String userEmail = _getUserEmail();
079        File input = _getFileInput(archiveName);
080        
081        getLogger().info("Importing archive {} ...", input.getAbsolutePath());
082        long t0 = System.currentTimeMillis();
083        
084        boolean success = true;
085        try
086        {
087            ImportReport importReport = _import(input, elements, merger);
088            
089            getLogger().info("Archive {} imported without error in {} ms", input.getAbsolutePath(), System.currentTimeMillis() - t0);
090            
091            // Once finished with import, inform user
092            String subject = _i18nUtils.translate(new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAIL_SUBJECT"));
093            String body = getSuccessMailBody(input, elements, importReport);
094            
095            _sendMail(subject, body, userEmail);
096            
097            success = importReport.getErrors().isEmpty();
098        }
099        catch (Exception e)
100        {
101            // Importing encountered an error during execution, send error mail
102            String subject = _i18nUtils.translate(new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAILERROR_SUBJECT"));
103            String body = _i18nUtils.translate(new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAILERROR_BODY"));
104            
105            _sendMail(subject, body, userEmail);
106            
107            // rethrow to ensure the job is marked as failed
108            throw e;
109        }
110        
111        // throw to ensure the job is marked as failed
112        if (!success)
113        {
114            throw new JobExecutionException("Errors occured during the import. See previous logs for more info.");
115        }
116    }
117    
118    private ImportReport _import(File input, Optional<Collection<String>> elements, Merger merger) throws IOException
119    {
120        if (elements.isPresent())
121        {
122            return _archiveHandler.partialImport(input, elements.get(), merger);
123        }
124        else
125        {
126            return _archiveHandler.importAll(input, merger);
127        }
128    }
129    
130    private File _getFileInput(String archiveName)
131    {
132        return _archiveHandler.getArchiveFile(archiveName);
133    }
134    
135    /**
136     * Gets the body of the success mail
137     * @param input The file input
138     * @param elements the imported elements or empty if all the archive was imported
139     * @param importReport The {@link ImportReport}
140     * @return the body of the success mail
141     * @throws IOException If an I/O error occurs with the ZIP filesystem
142     */
143    protected String getSuccessMailBody(File input, Optional<Collection<String>> elements, ImportReport importReport) throws IOException
144    {
145        final String catalogue = "plugin.contentio";
146        
147        Map<String, I18nizableTextParameter> i18nParameters = new HashMap<>();
148        
149        String inputPath = input.getCanonicalPath();
150        i18nParameters.put("archivePath", new I18nizableText(inputPath));
151        i18nParameters.put("archiveName", new I18nizableText(input.getName()));
152        
153        Collection<ImportError> errors = importReport.getErrors();
154        
155        StringBuilder body = new StringBuilder();
156        
157        if (errors.isEmpty())
158        {
159            body.append(_i18nUtils.translate(new I18nizableText(catalogue, "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAIL_SUCCESS_BODY_INTRO", i18nParameters)));
160        }
161        else if (errors.size() == 1)
162        {
163            body.append(_i18nUtils.translate(new I18nizableText(catalogue, "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAIL_SUCCESS_BODY_INTRO_WITH_ERROR", i18nParameters)));
164        }
165        else
166        {
167            i18nParameters.put("nbErrors", new I18nizableText(String.valueOf(errors.size())));
168            body.append(_i18nUtils.translate(new I18nizableText(catalogue, "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAIL_SUCCESS_BODY_INTRO_WITH_ERRORS", i18nParameters)));
169        }
170        
171        body.append("\n<br/><br/>");
172        
173        Predicate<Archiver> filterArchiver = elements.isPresent()
174                ? archiver -> !archiver.managedPartialImports(elements.get()).isEmpty()
175                : archiver -> true;
176        
177        // Join additional instruction
178        String additionalBody = _archiverEP.getExtensionsIds()
179                .stream()
180                .map(_archiverEP::getExtension)
181                .filter(filterArchiver)
182                .map(Archiver::additionalSuccessImportMail)
183                .flatMap(Collection::stream)
184                .distinct()
185                .map(_i18nUtils::translate)
186                .collect(Collectors.joining("<br/>"));
187        
188        if (!additionalBody.isBlank())
189        {
190            body.append("<br/>").append(additionalBody);
191        }
192        
193        if (!errors.isEmpty())
194        {
195            String errorItems = errors
196                    .stream()
197                    .map(importError -> String.format(__ERROR_TEMPLATE, importError.getMessage(), importError.getStackTrace()))
198                    .collect(Collectors.joining("\n"));
199            
200            body.append("\n<br/><br/>")
201                .append("\n" + _i18nUtils.translate(new I18nizableText(catalogue, "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_SCHEDULABLE_MAIL_BODY_LIST_OF_ERRORS")))
202                .append("\n<br/>")
203                .append("\n<ul>\n")
204                .append(errorItems)
205                .append("\n</ul>");
206        }
207        
208        return body.toString();
209    }
210}