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