/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.pinsetter.core.model;
import org.candlepin.auth.Principal;
import org.candlepin.model.AbstractHibernateObject;
import org.candlepin.pinsetter.core.PinsetterJobListener;
import org.candlepin.pinsetter.tasks.KingpinJob;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
* Represents the current status for a long-running job.
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
@Entity
@Table(name = JobStatus.DB_TABLE)
public class JobStatus extends AbstractHibernateObject {
/** Name of the table backing this object in the database */
public static final String DB_TABLE = "cp_job";
public static final String TARGET_TYPE = "target_type";
public static final String TARGET_ID = "target_id";
public static final String OWNER_ID = "owner_id";
public static final String CORRELATION_ID = "correlation_id";
public static final int RESULT_COL_LENGTH = 255;
/**
* Indicates possible states for a particular job.
*/
public enum JobState {
CREATED,
PENDING,
RUNNING,
FINISHED,
CANCELED,
FAILED,
WAITING; // added late, reordering would be problematic now
}
/**
* Types that a job can operate on
*/
public enum TargetType {
OWNER,
CONSUMER,
PRODUCT;
}
@Id
@Size(max = 255)
@NotNull
private String id;
@Column(length = 15)
@Size(max = 15)
private String jobGroup;
private JobState state;
private Date startTime;
private Date finishTime;
@Column(length = RESULT_COL_LENGTH)
@Size(max = RESULT_COL_LENGTH)
private String result;
@Size(max = 255)
private String principalName;
private TargetType targetType;
@Size(max = 255)
private String targetId;
@Size(max = 255)
private String ownerId;
@Column(length = 255)
private String correlationId;
@Column(length = 255)
private Class<? extends KingpinJob> jobClass;
private byte[] resultData;
@Transient
private boolean cloakData = false;
public JobStatus() { }
public JobStatus(JobDetail jobDetail) {
this(jobDetail, false);
}
public JobStatus(JobDetail jobDetail, boolean waiting) {
this.id = jobDetail.getKey().getName();
this.jobGroup = jobDetail.getKey().getGroup();
this.state = waiting ? JobState.WAITING : JobState.CREATED;
this.ownerId = this.getOwnerId(jobDetail);
this.targetType = getTargetType(jobDetail);
this.targetId = getTargetId(jobDetail);
this.principalName = getPrincipalName(jobDetail);
this.jobClass = getJobClass(jobDetail);
this.correlationId = getCorrelationId(jobDetail);
}
private String getPrincipalName(JobDetail detail) {
Principal p = (Principal) detail.getJobDataMap().get(
PinsetterJobListener.PRINCIPAL_KEY);
return p != null ? p.getPrincipalName() : "unknown";
}
private TargetType getTargetType(JobDetail jobDetail) {
return (TargetType) jobDetail.getJobDataMap().get(TARGET_TYPE);
}
private String getTargetId(JobDetail jobDetail) {
return (String) jobDetail.getJobDataMap().get(TARGET_ID);
}
private String getOwnerId(JobDetail jobDetail) {
return (String) jobDetail.getJobDataMap().get(OWNER_ID);
}
public String getCorrelationId(JobDetail jobDetail) {
return (String) jobDetail.getJobDataMap().get(CORRELATION_ID);
}
@SuppressWarnings("unchecked")
private Class<? extends KingpinJob> getJobClass(JobDetail jobDetail) {
return (Class<? extends KingpinJob>) jobDetail.getJobClass();
}
public void update(JobExecutionContext context) {
this.startTime = context.getFireTime();
long runTime = context.getJobRunTime();
if (this.startTime != null) {
setState(JobState.RUNNING);
if (runTime > -1) {
setState(JobState.FINISHED);
this.finishTime = new Date(startTime.getTime() + runTime);
}
}
else {
setState(JobState.PENDING);
}
Object jobResult = context.getResult();
if (jobResult != null) {
// TODO Check for instance of JobResult and set these appropriately
// Which will allow for a result message of sorts instead of just
// a class name resulting from Class.toString()
// BZ1004780: setResult truncates long strings
// setting result directly causes database issues.
setResult(jobResult.toString());
setResultData(jobResult);
}
}
public String getId() {
return id;
}
public String getGroup() {
return jobGroup;
}
public Date getFinishTime() {
return finishTime;
}
public String getResult() {
return result;
}
public Date getStartTime() {
return startTime;
}
public JobState getState() {
return state;
}
public void setState(JobState state) {
this.state = state;
}
public String getTargetType() {
if (targetType == null) {
return null;
}
return targetType.name().toLowerCase();
}
public String getTargetId() {
return targetId;
}
public String getCorrelationId() {
return correlationId;
}
public String getStatusPath() {
return "/jobs/" + this.id;
}
public void setResult(String result) {
// truncate the result to fit column
if (result == null || result.length() < RESULT_COL_LENGTH) {
this.result = result;
}
else {
this.result = result.substring(0, RESULT_COL_LENGTH);
}
}
public String getPrincipalName() {
return this.principalName;
}
public String getOwnerId() {
return this.ownerId;
}
@XmlTransient
public Class<? extends KingpinJob> getJobClass() {
return jobClass;
}
@XmlTransient
public JobKey getJobKey() {
return new JobKey(this.getId(), this.getGroup());
}
public boolean isDone() {
return this.state == JobState.CANCELED ||
this.state == JobState.FAILED ||
this.state == JobState.FINISHED;
}
public void setResultData(Object resultData) {
if (resultData == null) { return; }
byte[] data = new byte[0];
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(resultData);
data = baos.toByteArray();
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
finally {
try {
if (baos != null) {
baos.close();
}
if (oos != null) {
oos.close();
}
}
catch (Exception e) {
// unable to close streams
}
}
this.resultData = data;
}
public Object getResultData() {
if (this.resultData == null) { return null; }
if (this.cloakData) { return "[cloaked]"; }
Object result = null;
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
try {
bais = new ByteArrayInputStream(this.resultData);
ois = new ObjectInputStream(bais);
result = ois.readObject();
}
catch (Exception e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
finally {
try {
if (bais != null) {
bais.close();
}
if (ois != null) {
ois.close();
}
}
catch (Exception e) {
// unable to close streams
}
}
return result;
}
public JobStatus cloakResultData(boolean cloak) {
this.cloakData = cloak;
return this;
}
public String toString() {
return String.format("JobStatus [id: %s, type: %s, owner: %s, target: %s (%s), state: %s]",
this.id,
this.jobClass != null ? this.jobClass.getSimpleName() : null,
this.ownerId,
this.targetId,
this.targetType,
this.state
);
}
}