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.extraction.execution;
017
018import java.io.File;
019import java.io.IOException;
020import java.nio.file.Path;
021import java.time.ZonedDateTime;
022import java.time.format.DateTimeFormatter;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.regex.Pattern;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.commons.lang3.exception.ExceptionUtils;
032import org.quartz.JobDataMap;
033import org.quartz.JobExecutionContext;
034
035import org.ametys.cms.schedule.AbstractSendingMailSchedulable;
036import org.ametys.core.schedule.Schedulable;
037import org.ametys.core.schedule.progression.ContainerProgressionTracker;
038import org.ametys.core.ui.mail.StandardMailBodyHelper;
039import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
040import org.ametys.core.user.User;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.core.user.directory.NotUniqueUserException;
043import org.ametys.core.util.I18nUtils;
044import org.ametys.core.util.JSONUtils;
045import org.ametys.plugins.core.schedule.Scheduler;
046import org.ametys.plugins.extraction.ExtractionConstants;
047import org.ametys.plugins.extraction.execution.pipeline.PipelineDescriptor;
048import org.ametys.plugins.extraction.execution.pipeline.PipelineManager;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.util.AmetysHomeHelper;
052
053/**
054 * A {@link Schedulable} job which execute an extraction
055 */
056public class ExecuteExtractionSchedulable extends AbstractSendingMailSchedulable
057{
058    /** The key for the extraction definition file */
059    public static final String DEFINITION_FILE_PATH_KEY = "definitionFilePath";
060    /** The key for the variables values */
061    public static final String VARIABLES_KEY = "variables";
062    /** The key for the recipient */
063    public static final String RECIPIENT_KEY = "recipient";
064    /** The key for the pipeline */
065    public static final String PIPELINE_KEY = "pipeline";
066    
067    private static final String __JOBDATAMAP_DEFINITION_FILE_PATH_KEY = Scheduler.PARAM_VALUES_PREFIX + DEFINITION_FILE_PATH_KEY;
068    private static final String __JOBDATAMAP_VARIABLES_KEY = Scheduler.PARAM_VALUES_PREFIX + VARIABLES_KEY;
069    private static final String __JOBDATAMAP_RECIPIENT_KEY = Scheduler.PARAM_VALUES_PREFIX + RECIPIENT_KEY;
070    private static final String __JOBDATAMAP_PIPELINE_KEY = Scheduler.PARAM_VALUES_PREFIX + PIPELINE_KEY;
071
072    private static final String __RESULT_FILE_PATHS = "resultFilePaths";
073    
074    private static final DateTimeFormatter __RESULT_FILE_NAME_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd'T'HH.mm.ss");
075    
076    private JSONUtils _jsonUtils;
077    private PipelineManager _pipelineManager;
078    private ExtractionExecutor _extractionExecutor;
079    private ExecuteExtractionRunnableHelper _executeExtractionRunnableHelper;
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        super.service(manager);
085        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
086        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
087        _pipelineManager = (PipelineManager) manager.lookup(PipelineManager.ROLE);
088        _extractionExecutor = (ExtractionExecutor) manager.lookup(ExtractionExecutor.ROLE);
089        _executeExtractionRunnableHelper = (ExecuteExtractionRunnableHelper) manager.lookup(ExecuteExtractionRunnableHelper.ROLE);
090    }
091    
092    @Override
093    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
094    {
095        super.execute(context, progressionTracker);
096        
097        // Remove task corresponding to the runnable if there is one
098        String definitionFilePath = _getDefinitionFilePath(context);
099        _executeExtractionRunnableHelper.deleteRunnableJob(definitionFilePath);
100    }
101    
102    @Override
103    public void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
104    {
105        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
106
107        String definitionFilePath = _getDefinitionFilePath(context);
108        PipelineDescriptor pipeline = _pipelineManager.get(jobDataMap.getString(__JOBDATAMAP_PIPELINE_KEY));
109        String defaultResultFileName = _getDefaultResultFileName(definitionFilePath, pipeline);
110
111        Map<String, Object> parameters = _getExtractionParameters(jobDataMap);
112        String lang = (String) parameters.get("lang");
113        
114        Set<Path> resultFilePaths = _extractionExecutor.execute(definitionFilePath, defaultResultFileName, lang, parameters, pipeline);
115        context.put(__RESULT_FILE_PATHS, resultFilePaths);
116    }
117
118    private String _getDefaultResultFileName(String definitionFilePath, PipelineDescriptor pipeline)
119    {
120        String[] definitionFilePathSegments = definitionFilePath.split(Pattern.quote(File.separator));
121        String definitionFileName = definitionFilePathSegments[definitionFilePathSegments.length - 1];
122        
123        int lastIndexOfDot = definitionFileName.lastIndexOf('.');
124        if (-1 != lastIndexOfDot)
125        {
126            definitionFileName = definitionFileName.substring(0, lastIndexOfDot);
127        }
128        
129        String extractionDate = ZonedDateTime.now().format(__RESULT_FILE_NAME_DATE_TIME_FORMATTER);
130        
131        StringBuilder resultFileName = new StringBuilder();
132        resultFileName.append(definitionFileName).append("-").append(extractionDate);
133        resultFileName.append(".").append(pipeline.getDefaultExtension());
134        return resultFileName.toString();
135    }
136    
137    private Map<String, Object> _getExtractionParameters(JobDataMap jobDataMap)
138    {
139        // variables parameters
140        String variablesAsString = jobDataMap.getString(__JOBDATAMAP_VARIABLES_KEY);
141        Map<String, Object> variablesAsMap = _jsonUtils.convertJsonToMap(variablesAsString);
142        return variablesAsMap;
143    }
144    
145    @Override
146    protected String _getRecipientLanguage(JobExecutionContext context)
147    {
148        User user = _getUser(context);
149        if (user != null)
150        {
151            return user.getLanguage();
152        }
153        return null;
154    }
155    
156    private User _getUser(JobExecutionContext context)
157    {
158        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
159        String recipient = jobDataMap.getString(__JOBDATAMAP_RECIPIENT_KEY);
160        
161        if (jobDataMap.containsKey("userIdentity"))
162        {
163            String userIdentityString = jobDataMap.getString("userIdentity");
164            UserIdentity userIdentity = UserIdentity.stringToUserIdentity(userIdentityString);
165
166            // Try to retrieve the user that launched the job
167            User user = _userManager.getUser(userIdentity);
168            // If the launching user is also the recipient, get its language
169            if (user != null && recipient != null && recipient.equals(user.getEmail()))
170            {
171                return user;
172            }
173            else
174            {
175                // Try to retrieve the recipient by its mail in the population of the launching user
176                try
177                {
178                    return _userManager.getUserByEmail(userIdentity.getPopulationId(), recipient);
179                }
180                catch (NotUniqueUserException e)
181                {
182                    // Do Nothing
183                }
184            }
185        }
186        
187        return null;
188    }
189    
190    @Override
191    protected Optional<String> _getRecipient(JobExecutionContext context)
192    {
193        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
194        return Optional.ofNullable(jobDataMap.getString(__JOBDATAMAP_RECIPIENT_KEY))
195                .filter(StringUtils::isNotEmpty);
196    }
197    
198    @Override
199    protected boolean _isMailBodyInHTML(JobExecutionContext context)
200    {
201        return true;
202    }
203    
204    @Override
205    protected I18nizableText _getSuccessMailSubject(JobExecutionContext context)
206    {
207        String extractionName = _getDefinitionFilePath(context);
208        return new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_SUBJECT", Map.of("extractionName", new I18nizableText(extractionName)));
209    }
210    
211    @Override
212    protected String _getSuccessMailBody(JobExecutionContext context, String language)
213    {
214        try
215        {
216            String extractionFilePath = _getDefinitionFilePath(context);
217            
218            MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
219                .withTitle(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_TITLE", Map.of("extractionName", new I18nizableText(extractionFilePath))))
220                .withLanguage(language)
221                .addMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY", Map.of("extractionName", new I18nizableText(extractionFilePath))));
222            
223            I18nizableText resultsToolLabel = new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_RESULTS_LIST_TOOL_LABEL");
224
225            @SuppressWarnings("unchecked")
226            Set<Path> resultFilePaths = (Set<Path>) context.get(__RESULT_FILE_PATHS);
227            if (resultFilePaths.size() == 1)
228            {
229                String downloadLink = _getResultFileDownloadLink(resultFilePaths.iterator().next());
230                
231                bodyBuilder
232                    .addMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_DOWNLOAD_RESULT", Map.of("link", new I18nizableText(downloadLink))))
233                    .addMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_SEE_RESULT", Map.of("toolLabel", resultsToolLabel)));
234            }
235            else
236            {
237                StringBuilder downloadLinks = new StringBuilder("<ul>");
238                for (Path resultFilePath : resultFilePaths)
239                {
240                    downloadLinks.append("<li>")
241                                 .append(_getResultFileDownloadLink(resultFilePath))
242                                 .append("</li>");
243                }
244                downloadLinks.append("</ul>");
245                
246                bodyBuilder
247                    .addMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_DOWNLOAD_RESULTS", Map.of("links", new I18nizableText(downloadLinks.toString()))))
248                    .addMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_SEE_RESULTS", Map.of("toolLabel", resultsToolLabel)));
249            }
250            
251            bodyBuilder.withLink(Config.getInstance().getValue("cms.url"), new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_SUCCESS_BODY_CMS_LINK_LABEL"));
252            
253            return bodyBuilder.build();
254        }
255        catch (IOException e)
256        {
257            getLogger().error("Failed to build HTML body for extraction report mail", e);
258            return null;
259        }
260    }
261    
262    @Override
263    protected I18nizableText _getErrorMailSubject(JobExecutionContext context)
264    {
265        String extractionName = _getDefinitionFilePath(context);
266        return new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_ERROR_SUBJECT", Map.of("extractionName", new I18nizableText(extractionName)));
267    }
268    
269    @Override
270    protected String _getErrorMailBody(JobExecutionContext context, String language, Throwable throwable)
271    {
272        String extractionName = _getDefinitionFilePath(context);
273        String error = ExceptionUtils.getStackTrace(throwable);
274        
275        try
276        {
277            return StandardMailBodyHelper.newHTMLBody()
278                .withTitle(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_ERROR_BODY_TITLE", Map.of("extractionName", new I18nizableText(extractionName))))
279                .withMessage(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_ERROR_BODY", Map.of("extractionName", new I18nizableText(extractionName))))
280                .withDetails(new I18nizableText(ExtractionConstants.PLUGIN_NAME, "PLUGINS_EXTRACTION_EXECUTE_EXTRACTION_MAIL_ERROR_TITLE"), error, true)
281                .withLanguage(language)
282                .build();
283        }
284        catch (IOException e)
285        {
286            getLogger().error("Failed to build HTML body email for extraction report results", e);
287            return null;
288        }
289    }
290    
291    private String _getDefinitionFilePath(JobExecutionContext context)
292    {
293        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
294        return jobDataMap.getString(__JOBDATAMAP_DEFINITION_FILE_PATH_KEY);
295    }
296    
297    private String _getResultFileDownloadLink(Path resultFilePath)
298    {
299        Path rootPath = AmetysHomeHelper.getAmetysHomeData().toPath().resolve(ExtractionConstants.RESULT_EXTRACTION_DIR_NAME);
300        
301        String rootURI = rootPath.toUri().toString();
302        String resultFileURI = resultFilePath.toUri().toString();
303        
304        String resultFileRelativeURI = resultFileURI.substring(rootURI.length());
305        String downloadURL = Config.getInstance().getValue("cms.url") + "/plugins/extraction/download/result/" + resultFileRelativeURI;
306        String resultFileRelativePath = rootPath.relativize(resultFilePath).toString();
307
308        return "<a href=\"" + downloadURL + "\">" + resultFileRelativePath + "</a>";
309    }
310    
311}