/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright 2006 - 2009 Pentaho Corporation. All rights reserved.
*
*/
package org.pentaho.platform.scheduler;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.platform.api.engine.IActionSequence;
import org.pentaho.platform.api.engine.IBackgroundExecution;
import org.pentaho.platform.api.engine.IContentGenerator;
import org.pentaho.platform.api.engine.IFileInfo;
import org.pentaho.platform.api.engine.IOutputHandler;
import org.pentaho.platform.api.engine.IParameterProvider;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.engine.IPluginManager;
import org.pentaho.platform.api.repository.IContentItem;
import org.pentaho.platform.api.repository.IContentRepository;
import org.pentaho.platform.api.repository.ISolutionRepository;
import org.pentaho.platform.api.scheduler.BackgroundExecutionException;
import org.pentaho.platform.api.scheduler.IJobDetail;
import org.pentaho.platform.engine.core.solution.ActionInfo;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.core.system.UserSession;
import org.pentaho.platform.engine.services.solution.StandardSettings;
import org.pentaho.platform.repository.content.ContentRepository;
import org.pentaho.platform.repository.content.CoreContentRepositoryOutputHandler;
import org.pentaho.platform.repository.hibernate.HibernateUtil;
import org.pentaho.platform.scheduler.messages.Messages;
import org.pentaho.platform.util.UUIDUtil;
import org.pentaho.platform.util.logging.Logger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
public class QuartzBackgroundExecutionHelper implements IBackgroundExecution {
public static final String DEFAULT_JOB_NAME = "bgExecution"; //$NON-NLS-1$
public static final String DEFAULT_TRIGGER_NAME = "bgTrigger"; //$NON-NLS-1$
public static final String DEFAULT_BACKGROUND_LOCATION = "background"; //$NON-NLS-1$
public static final String BACKGROUND_USER_NAME_STR = "background_user_name"; //$NON-NLS-1$
public static final String BACKGROUND_CONTENT_GUID_STR = "background_output_content_guid"; //$NON-NLS-1$
public static final String BACKGROUND_CONTENT_LOCATION_STR = "background_output_location"; //$NON-NLS-1$
public static final String BACKGROUND_CONTENT_COOKIE_PREFIX = "pentaho_background_content"; //$NON-NLS-1$
public static final String BACKGROUND_EXECUTION_FLAG = "backgroundExecution"; //$NON-NLS-1$
private static final Log logger = LogFactory.getLog(QuartzBackgroundExecutionHelper.class);
/*
* **************************** Methods from the Interface****************************
*/
/**
* NOTE: client code is responsible for making sure a job with the name identified by the parameter StandardSettings.SCHEDULE_NAME in the parameter provider
* does not already exist in the quartz scheduler. If such a job does already exist,
*
* @param parameterProvider
* IParameterProvider expected to have the following parameters: required: solution path action optional (cron-string is required to create a
* CronTrigger): cron-string repeat-count repeat-time-milliseconds start-date end-date
*
*/
public String backgroundExecuteAction(IPentahoSession userSession, IParameterProvider parameterProvider) throws BackgroundExecutionException {
try {
String cronString = parameterProvider.getStringParameter(StandardSettings.CRON_STRING, null);
String repeatInterval = parameterProvider.getStringParameter(StandardSettings.REPEAT_TIME_MILLISECS, null);
assert (repeatInterval == null && cronString != null) || (repeatInterval != null && cronString == null) || (repeatInterval == null && cronString == null) : Messages.getInstance()
.getErrorString("QuartzBackgroundExecutionHelper.ERROR_0423_INVALID_INTERVAL"); //$NON-NLS-1$
String solutionName = parameterProvider.getStringParameter(StandardSettings.SOLUTION, null); //$NON-NLS-1$
String actionPath = parameterProvider.getStringParameter(StandardSettings.PATH, null); //$NON-NLS-1$
String actionName = parameterProvider.getStringParameter(StandardSettings.ACTION, null); //$NON-NLS-1$
String actionSeqPath = parameterProvider.getStringParameter(StandardSettings.ACTIONS_REF, null);
if (actionSeqPath == null || actionSeqPath.length() <= 0) {
actionSeqPath = solutionName + ISolutionRepository.SEPARATOR + actionPath + ISolutionRepository.SEPARATOR + actionName;
}
actionSeqPath = cleanActionPath(actionSeqPath);
String description = parameterProvider.getStringParameter(StandardSettings.DESCRIPTION, null);
String scheduleName = null;
String scheduleGroupName = null;
String outputContentGUID = UUIDUtil.getUUIDAsString();
if ((null == cronString) && (null == repeatInterval)) {
// must be a quick one-shot background schedule
scheduleName = outputContentGUID;
scheduleGroupName = getUserName(userSession);
} else {
// must be some kind of repeating or cron schedule
scheduleName = parameterProvider.getStringParameter(StandardSettings.SCHEDULE_NAME, null);
scheduleGroupName = parameterProvider.getStringParameter(StandardSettings.SCHEDULE_GROUP_NAME, null);
}
JobDetail jobDetail = createDetailFromParameterProvider(parameterProvider, userSession, outputContentGUID, scheduleName, scheduleGroupName, description,
actionSeqPath);
// stores the user name and outputContentGUID in the Content Repository's persistent store (e.g. a database via hibernate)
trackBackgroundExecution(userSession, outputContentGUID);
Scheduler sched = QuartzSystemListener.getSchedulerInstance();
Trigger bgTrigger = null;
if (null != cronString) {
String startDate = parameterProvider.getStringParameter(StandardSettings.START_DATE_TIME, null);
String endDate = parameterProvider.getStringParameter(StandardSettings.END_DATE_TIME, null);
bgTrigger = SchedulerHelper.createCronTrigger(scheduleName, scheduleGroupName, startDate, endDate, cronString);
} else if (null != repeatInterval) {
String startDate = parameterProvider.getStringParameter(StandardSettings.START_DATE_TIME, null);
String endDate = parameterProvider.getStringParameter(StandardSettings.END_DATE_TIME, null);
String repeatCount = parameterProvider.getStringParameter(StandardSettings.REPEAT_COUNT, null);
bgTrigger = SchedulerHelper.createRepeatTrigger(scheduleName, scheduleGroupName, startDate, endDate, repeatCount, repeatInterval);
} else {
// listener's name (as returned by listener.getName() will be the value of outputContentGUID
// the listener arranges for a flag to be set in the user's session, and can be used by web UI
// to inform user that job has completed
BackgroundExecuteListener listener = new BackgroundExecuteListener(userSession, scheduleName, sched, jobDetail.getName());
sched.addJobListener(listener);
jobDetail.addJobListener(listener.getName());
bgTrigger = new SimpleTrigger(scheduleName, scheduleGroupName); // trigger fires now (or as soon as possible)
}
sched.scheduleJob(jobDetail, bgTrigger);
// TODO: Fix with properly formatted HTML template for this status message. (<--this comment is incorrect)
// Keep in mind that this class should be UI agnostic, and it should be the UI layer
// that creates the UI-technology-appropriate message. (The HTML content of this message
// should NOT be in this class.)
return Messages.getInstance()
.getString(
"BackgroundExecuteHelper.USER_JOB_SUBMITTED", "UserContent", "if(window.opener) {window.opener.location.href='UserContent'; window.close() } else { return true; }"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} catch (SchedulerException ex) {
throw new BackgroundExecutionException(Messages.getInstance().getErrorString("QuartzBackgroundExecutionHelper.ERROR_0421_UNABLE_TO_SUBMIT_USER_JOB", ex.getLocalizedMessage()), ex);
} catch (ParseException ex) {
throw new BackgroundExecutionException(Messages.getInstance().getErrorString("QuartzBackgroundExecutionHelper.ERROR_0422_INVALID_DATE_FORMAT", ex.getLocalizedMessage()), ex);
}
}
public void trackBackgroundExecution(IPentahoSession userSession, String GUID) {
IContentRepository repo = ContentRepository.getInstance(userSession);
repo.newBackgroundExecutedContentId(userSession, GUID);
}
public IContentItem getBackgroundContent(String contentGUID, IPentahoSession userSession) {
IContentRepository repo = ContentRepository.getInstance(userSession);
try {
IContentItem item = repo.getContentItemById(contentGUID);
return item;
} catch (Exception ex) {
Logger.error(this.getClass().getName(), ex.getLocalizedMessage(), ex);
}
return null;
}
public List<IJobDetail> getScheduledAndExecutingBackgroundJobs(IPentahoSession userSession) throws BackgroundExecutionException {
try {
Scheduler sched = QuartzSystemListener.getSchedulerInstance();
String userName = getUserName(userSession);
String[] jobNames = sched.getJobNames(userName); // can throw SchedulerException
List<IJobDetail> rtn = new ArrayList<IJobDetail>();
if (jobNames != null) {
for (int i = 0; i < jobNames.length; i++) {
JobDetail jobDetail = sched.getJobDetail(jobNames[i], userName); // can throw SchedulerException
rtn.add(new QuartzJobDetail(jobDetail));
}
}
return rtn;
} catch (SchedulerException ex) {
throw new BackgroundExecutionException(Messages.getInstance().getErrorString("QuartzBackgroundExecutionHelper.ERROR_0420_FAILED_TO_GET_JOBS_FROM_SCHEDULER", ex.getLocalizedMessage()), ex);
}
}
public void removeBackgroundExecutedContentForID(String contentGUID, IPentahoSession userSession) {
// First, remove content item from the repo with that GUID.
IContentRepository repo = ContentRepository.getInstance(userSession);
try {
IContentItem item = repo.getContentItemById(contentGUID);
if (item != null) {
item.makeTransient();
} else {
return;
}
} finally {
HibernateUtil.commitTransaction();
}
repo.removeBackgroundExecutedContentId(userSession, contentGUID);
}
public List getBackgroundExecutedContentList(IPentahoSession userSession) {
IContentRepository repo = ContentRepository.getInstance(userSession);
ArrayList idList = new ArrayList();
List idObjectList = repo.getBackgroundExecutedContentItemsForUser(userSession);
if (idObjectList != null) {
IContentItem contentItem = null;
for (int i = 0; i < idObjectList.size(); i++) {
contentItem = (IContentItem) idObjectList.get(i);
idList.add(contentItem);
}
}
return idList;
}
// Helper Utility Methods
/**
* @param parameterProvider
* @param userSession
* @param outputContentGUID
* String will be used as the job name in Quartz
* @param jobGroup
* String will be used as the job group name in Quartz
* @param solutionName
* @param actionPath
* @param actionName
*/
protected JobDetail createDetailFromParameterProvider(IParameterProvider parameterProvider, IPentahoSession userSession, String outputContentId,
String jobName, String jobGroup, String description, String actionSeqPath) {
JobDetail jobDetail = new JobDetail(jobName, jobGroup, QuartzExecute.class);
if (null != description) {
jobDetail.setDescription(description);
}
JobDataMap data = jobDetail.getJobDataMap();
Iterator<String> inputNamesIterator = parameterProvider.getParameterNames();
while (inputNamesIterator.hasNext()) {
String inputName = (String) inputNamesIterator.next();
Object inputValue = parameterProvider.getParameter(inputName);
data.put(inputName, inputValue);
}
ActionInfo actionInfo = ActionInfo.parseActionString(actionSeqPath);
String actionName = actionInfo.getActionName();
int lastDot = actionName.lastIndexOf('.');
String type = actionName.substring(lastDot + 1);
IPluginManager pluginManager = PentahoSystem.get(IPluginManager.class, userSession);
IContentGenerator generator = null;
try {
generator = pluginManager.getContentGeneratorForType(type, userSession);
} catch (Exception ignored) {
// don't let a bad plugin situation take us down
logger.warn(ignored.getMessage(), ignored);
}
ISolutionRepository repo = PentahoSystem.get(ISolutionRepository.class, userSession);
String title = jobName;
if (generator != null) {
// get the title for the plugin file
InputStream inputStream = null;
try {
inputStream = repo.getResourceInputStream(actionSeqPath, true, ISolutionRepository.ACTION_EXECUTE);
} catch (FileNotFoundException e) {
logger.warn(e.getMessage(), e);
// proceed to get the file info from the plugin manager. getFileInfo will return a failsafe fileInfo when something goes wrong.
}
IFileInfo fileInfo = pluginManager.getFileInfo(type, userSession, repo.getSolutionFile(actionSeqPath, ISolutionRepository.ACTION_EXECUTE), inputStream);
title = fileInfo.getTitle();
} else {
IActionSequence action = repo.getActionSequence(actionInfo.getSolutionName(), actionInfo.getPath(), actionInfo.getActionName(), repo.getLoggingLevel(),
ISolutionRepository.ACTION_EXECUTE);
title = action.getTitle();
}
data.put(BACKGROUND_ACTION_NAME_STR, title);
data.put("processId", this.getClass().getName()); //$NON-NLS-1$
data.put(BACKGROUND_USER_NAME_STR, getUserName(userSession));
data.put(BACKGROUND_CONTENT_GUID_STR, outputContentId);
data.put(BACKGROUND_CONTENT_LOCATION_STR, DEFAULT_BACKGROUND_LOCATION + "/" + UUIDUtil.getUUIDAsString()); //$NON-NLS-1$
SimpleDateFormat fmt = new SimpleDateFormat();
data.put(BACKGROUND_SUBMITTED, fmt.format(new Date()));
data.put(StandardSettings.SOLUTION, actionInfo.getSolutionName());
data.put(StandardSettings.PATH, actionInfo.getPath());
data.put(StandardSettings.ACTION, actionInfo.getActionName());
// This tells our execution component (QuartzExecute) that we're running a background job instead of
// a standard quartz execution.
data.put(BACKGROUND_EXECUTION_FLAG, "true"); //$NON-NLS-1$
return jobDetail;
}
public IPentahoSession getEffectiveUserSession(final String user) {
UserSession us = new UserSession(user, null, true, null);
return us;
}
public static class BackgroundExecuteListener implements JobListener {
private IPentahoSession userSession;
private String contentGUID;
private Scheduler sched;
private String jobName;
public BackgroundExecuteListener(IPentahoSession session, String contentGUID, Scheduler scheduler, String jobName) {
userSession = session;
this.contentGUID = contentGUID;
this.jobName = jobName;
this.sched = scheduler;
}
public String getName() {
return contentGUID;
}
public void jobExecutionVetoed(JobExecutionContext context) {
// TODO Auto-generated method stub
}
public void jobToBeExecuted(JobExecutionContext context) {
// TODO Auto-generated method stub
}
public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
// Update the userSession with the updated content item.
JobDetail ctxDetail = context.getJobDetail();
if ((ctxDetail != null) && (ctxDetail.getName().equals(this.jobName))) { // Only do if it's for our job...
Object contentItemGUID = ctxDetail.getJobDataMap().get(BACKGROUND_CONTENT_GUID_STR);
if (contentItemGUID != null && userSession != null) {
userSession.setBackgroundExecutionAlert(); // Toggle the alert status
} else if (contentItemGUID == null) {
Logger.warn(this.getClass().getName(), Messages.getInstance().getString("BackgroundExecuteHelper.WARN_CONTENT_ITEM_NOT_CREATED")); //$NON-NLS-1$
}
this.userSession = null; // Make sure nothing keeps a handle to the user session.
try {
if (sched != null) {
sched.removeJobListener(this.getName());
}
} catch (RuntimeException ex) {
throw ex; // programmer error, let RuntimeExceptions leak
} catch (Exception ex) {
logger.error(Messages.getInstance().getErrorString("BackgroundExecuteHelper.ERROR_0002_REMOVE_LISTENER_FAILED"), ex); //$NON-NLS-1$
}
}
}
}
public IOutputHandler getContentOutputHandler(final String location, final String fileName, final String solutionName, final IPentahoSession userSession,
final IParameterProvider parameterProvider) {
return new CoreContentRepositoryOutputHandler(location, fileName, solutionName, userSession);
}
private static String getUserName(IPentahoSession userSession) {
return userSession.isAuthenticated() ? userSession.getName() : IBackgroundExecution.DEFAULT_USER_NAME;
}
/*
* cleanActionPath cleans action path like steelswheels////myreport.xaction to steelwheels/myreport.xaction
* Note - the ISolutionRepository.SEPARATOR could be used here, but it's hardcoded to
* forward slash and has been for two years. So - Changing to be clean with no
* additional temporary object allocations. MB 2009-08-13
*/
private String cleanActionPath(String s) {
while (s.indexOf("//")>=0) { //$NON-NLS-1$
return cleanActionPath(s.replaceAll("//", "/")); //$NON-NLS-1$ //$NON-NLS-2$
}
return s;
}
}