001/*
002 *  Copyright 2016 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.core.schedule;
017
018import java.time.Duration;
019import java.time.Instant;
020import java.util.Map;
021
022import org.apache.avalon.framework.context.Context;
023import org.apache.avalon.framework.context.ContextException;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.cocoon.Constants;
027import org.apache.cocoon.environment.ObjectModelHelper;
028import org.apache.cocoon.environment.Request;
029import org.apache.cocoon.environment.background.BackgroundEnvironment;
030import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
031import org.quartz.DisallowConcurrentExecution;
032import org.quartz.Job;
033import org.quartz.JobDataMap;
034import org.quartz.JobDetail;
035import org.quartz.JobExecutionContext;
036import org.quartz.JobExecutionException;
037import org.quartz.JobKey;
038import org.quartz.PersistJobDataAfterExecution;
039import org.quartz.SchedulerException;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import org.ametys.core.authentication.AuthenticateAction;
044import org.ametys.core.engine.BackgroundEngineHelper;
045import org.ametys.core.schedule.Runnable.FireProcess;
046import org.ametys.core.user.population.UserPopulationDAO;
047import org.ametys.plugins.core.schedule.Scheduler;
048import org.ametys.plugins.core.user.UserDAO;
049
050/**
051 * Ametys implementation of a {@link Job} which delegates the execution of the task to the right {@link Schedulable}
052 */
053@PersistJobDataAfterExecution
054@DisallowConcurrentExecution
055public class AmetysJob implements Job
056{
057    /** The key for the last duration of the {@link #execute(org.quartz.JobExecutionContext)} method which is stored in the {@link JobDataMap} */
058    public static final String KEY_LAST_DURATION = "duration";
059    /** The key for the previous fire time of this job which is stored in the {@link JobDataMap} */
060    public static final String KEY_PREVIOUS_FIRE_TIME = "previousFireTime";
061    /** The key for the success state of the last execution of the job */
062    public static final String KEY_SUCCESS = "success";
063    
064    /** The service manager */
065    protected static ServiceManager _serviceManager;
066    /** The extension point for {@link Schedulable}s */
067    protected static SchedulableExtensionPoint _schedulableEP;
068    /** The scheduler component */
069    protected static Scheduler _scheduler;
070    /** The cocoon environment context. */
071    protected static org.apache.cocoon.environment.Context _environmentContext;
072    
073    /**
074     * Initialize the static fields.
075     * @param serviceManager The service manager
076     * @param context The context
077     * @throws ServiceException if an error occurs during the lookup of the {@link SchedulableExtensionPoint}
078     * @throws ContextException if environment context object not found
079     */
080    public static void initialize(ServiceManager serviceManager, Context context) throws ServiceException, ContextException
081    {
082        _serviceManager = serviceManager;
083        _schedulableEP = (SchedulableExtensionPoint) serviceManager.lookup(SchedulableExtensionPoint.ROLE);
084        _scheduler = (Scheduler) serviceManager.lookup(Scheduler.ROLE);
085        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
086    }
087    
088    /**
089     * Releases and destroys used resources
090     */
091    public static void dispose()
092    {
093        _serviceManager = null;
094        _schedulableEP = null;
095        _scheduler = null;
096        _environmentContext = null;
097    }
098    
099    @Override
100    public void execute(JobExecutionContext context) throws JobExecutionException
101    {
102        JobDetail detail = context.getJobDetail();
103        JobDataMap jobDataMap = detail.getJobDataMap();
104        JobKey jobKey = detail.getKey();
105        String runnableId = jobDataMap.getString(Scheduler.KEY_RUNNABLE_ID);
106        String schedulableId = jobDataMap.getString(Scheduler.KEY_SCHEDULABLE_ID);
107        Schedulable schedulable = _schedulableEP.getExtension(schedulableId);
108        Logger logger = LoggerFactory.getLogger(AmetysJob.class.getName() + "$" + schedulableId);
109        
110        // Concurrency allowed ?
111        if (!schedulable.acceptConcurrentExecution() && _checkConcurrency(schedulableId, runnableId, jobKey, logger))
112        {
113            logger.warn("The Runnable '{}' of the Schedulable '{}' cannot be executed now because concurrent execution is not allowed for this Schedulable", runnableId, schedulableId);
114            return;
115        }
116        
117        boolean success = true;
118        
119        // Set the previous (which is actually the current) fire time in the map (because the trigger may no longer exist in the future)
120        jobDataMap.put(KEY_PREVIOUS_FIRE_TIME, context.getTrigger().getPreviousFireTime().getTime()); // possible with @PersistJobDataAfterExecution annotation
121        
122        Map<String, Object> environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_serviceManager, _environmentContext, new SLF4JLoggerAdapter(logger));
123        
124        _setUserSystemInSession(environmentInformation);
125        
126        logger.info("Executing the Runnable '{}' of the Schedulable '{}' with jobDataMap:\n '{}'", runnableId, schedulableId, jobDataMap.getWrappedMap().toString());
127        Instant start = Instant.now();
128        try
129        {
130            schedulable.execute(context);
131        }
132        catch (Exception e)
133        {
134            success = false;
135            logger.error(String.format("An exception occured during the execution of the Schedulable '%s'", schedulableId), e);
136            throw new JobExecutionException(String.format("An exception occured during the execution of the job '%s'", schedulableId), e);
137        }
138        catch (Throwable t)
139        {
140            success = false;
141            logger.error(String.format("An error occured during the execution of the Schedulable '%s'", schedulableId), t);
142            throw t;
143        }
144        finally
145        {
146            // Set the duration in the map
147            Instant end = Instant.now();
148            long duration = Duration.between(start, end).toMillis();
149            jobDataMap.put(KEY_LAST_DURATION, duration); // possible with @PersistJobDataAfterExecution annotation
150            
151            // Leave the Engine Environment
152            if (environmentInformation != null)
153            {
154                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
155            }
156            
157            // Success ?
158            jobDataMap.put(KEY_SUCCESS, success);
159            
160            // Run at startup tasks are one-shot tasks => if so, indicates it is completed for never refiring it
161            if (FireProcess.STARTUP.toString().equals(jobDataMap.getString(Scheduler.KEY_RUNNABLE_FIRE_PROCESS)))
162            {
163                jobDataMap.put(Scheduler.KEY_RUNNABLE_STARTUP_COMPLETED, true);
164            }
165        }
166    }
167    
168    private void _setUserSystemInSession (Map<String, Object> environmentInformation)
169    {
170        BackgroundEnvironment bgEnv = (BackgroundEnvironment) environmentInformation.get("environment");
171        Request request = ObjectModelHelper.getRequest(bgEnv.getObjectModel());
172        
173        AuthenticateAction.setUserIdentityInSession(request, UserPopulationDAO.SYSTEM_USER_IDENTITY, new UserDAO.ImpersonateCredentialProvider(), true);
174    }
175    
176    private boolean _checkConcurrency(String schedulableId, String runnableId, JobKey currentJobKey, Logger logger) throws JobExecutionException
177    {
178        try
179        {
180            return _scheduler.getScheduler().getCurrentlyExecutingJobs().stream()
181                    .map(JobExecutionContext::getJobDetail)
182                    .filter(detail -> !detail.getKey().equals(currentJobKey)) // currently executing without this
183                    .map(detail -> detail.getJobDataMap().getString(Scheduler.KEY_SCHEDULABLE_ID))
184                    .filter(id -> id.equals(schedulableId))
185                    .findFirst()
186                    .isPresent();
187        }
188        catch (SchedulerException e)
189        {
190            logger.error(String.format("An error occured during the concurrency check of the Runnable '%s'", runnableId), e);
191            throw new JobExecutionException(String.format("An error occured during the concurrency check of the Runnable '%s'", runnableId), e);
192        }
193    }
194}