/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* 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 General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.core.domain.operation;
import java.io.Serializable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.rhq.core.domain.configuration.Configuration;
/**
* The entity that represents an operation invocation that is either in progress or has completed. This is the
* superclass to both the the individual resource history object (that encapsulates the results) and the group history
* object (for group invocations across multiple resources).
*
* @author John Mazzitelli
*/
@DiscriminatorColumn(name = "DTYPE")
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NamedQueries( {
@NamedQuery(name = OperationHistory.QUERY_FIND_BY_JOB_ID, query = "SELECT h FROM OperationHistory h LEFT JOIN FETCH h.parameters hp "
+ " WHERE h.jobName = :jobName " + " AND h.jobGroup = :jobGroup " + " AND h.createdTime = :createdTime"),
@NamedQuery(name = OperationHistory.QUERY_GET_RECENTLY_COMPLETED_RESOURCE_ADMIN, query = "SELECT DISTINCT new org.rhq.core.domain.operation.composite.ResourceOperationLastCompletedComposite( "
+ " ro.id, "
+ " ro.operationDefinition.displayName, "
+ " ro.createdTime, "
+ " ro.status, "
+ " ro.resource.id, "
+ " ro.resource.resourceType.id, "
+ " ro.resource.name, "
+ " ro.resource.ancestry) "
+ " FROM ResourceOperationHistory ro "
+ " WHERE ro.status != 'INPROGRESS' "
+ " AND ( ro.resource.id = :resourceId OR :resourceId IS NULL )"),
@NamedQuery(name = OperationHistory.QUERY_GET_RECENTLY_COMPLETED_RESOURCE, query = "SELECT DISTINCT new org.rhq.core.domain.operation.composite.ResourceOperationLastCompletedComposite( "
+ " ro.id, "
+ " ro.operationDefinition.displayName, "
+ " ro.createdTime, "
+ " ro.status, "
+ " ro.resource.id, "
+ " ro.resource.resourceType.id, "
+ " ro.resource.name, "
+ " ro.resource.ancestry) "
+ " FROM ResourceOperationHistory ro JOIN ro.resource.implicitGroups g JOIN g.roles r JOIN r.subjects s "
+ " WHERE ro.status != 'INPROGRESS' AND s = :subject "
+ " AND ( ro.resource.id = :resourceId OR :resourceId IS NULL )"),
@NamedQuery(name = OperationHistory.QUERY_GET_RECENTLY_COMPLETED_GROUP_ADMIN, query = "SELECT DISTINCT new org.rhq.core.domain.operation.composite.GroupOperationLastCompletedComposite( "
+ " go.id, "
+ " go.operationDefinition.displayName, "
+ " go.createdTime, "
+ " go.status, "
+ " go.group.id, "
+ " go.group.name, "
+ " go.group.resourceType.name) "
+ " FROM GroupOperationHistory go " + " WHERE go.status != 'INPROGRESS' "),
@NamedQuery(name = OperationHistory.QUERY_GET_RECENTLY_COMPLETED_GROUP, query = "SELECT DISTINCT new org.rhq.core.domain.operation.composite.GroupOperationLastCompletedComposite( "
+ " go.id, "
+ " go.operationDefinition.displayName, "
+ " go.createdTime, "
+ " go.status, "
+ " g.id, "
+ " g.name, "
+ " g.resourceType.name) "
+ " FROM GroupOperationHistory go JOIN go.group g JOIN g.roles r JOIN r.subjects s "
+ " WHERE go.status != 'INPROGRESS' AND s = :subject "),
@NamedQuery(name = OperationHistory.QUERY_GET_PARAMETER_CONFIGURATION_IDS, query = "" //
+ "SELECT params.id " //
+ " FROM OperationHistory oh " // both resource and group operations have parameters
+ " JOIN oh.parameters params " //
+ " WHERE oh.id = :historyId"), //
@NamedQuery(name = OperationHistory.QUERY_GET_RESULT_CONFIGURATION_IDS, query = "" //
+ "SELECT res.id " //
+ " FROM ResourceOperationHistory roh " // only resource operations have results
+ " JOIN roh.results res " //
+ " WHERE roh.id = :historyId"), //
@NamedQuery(name = OperationHistory.QUERY_DELETE_BY_HISTORY_IDS, query = "" //
+ "DELETE FROM OperationHistory oh " //
+ " WHERE oh.id = :historyId "),
@NamedQuery(name = OperationHistory.QUERY_DELETE_BY_DEFINITION, query = "" //
+ "DELETE FROM OperationHistory oh "
+ " WHERE oh.operationDefinition = :definition"
)
})
@SequenceGenerator(allocationSize = org.rhq.core.domain.util.Constants.ALLOCATION_SIZE, name = "RHQ_OPERATION_HISTORY_ID_SEQ", sequenceName = "RHQ_OPERATION_HISTORY_ID_SEQ")
@Table(name = "RHQ_OPERATION_HISTORY")
public abstract class OperationHistory implements Serializable {
public static final String QUERY_FIND_BY_JOB_ID = "OperationHistory.findByJobId";
public static final String QUERY_GET_RECENTLY_COMPLETED_RESOURCE = "OperationHistory.getRecentlyCompletedResource";
public static final String QUERY_GET_RECENTLY_COMPLETED_RESOURCE_ADMIN = "OperationHistory.getRecentlyCompletedResource_admin";
public static final String QUERY_GET_RECENTLY_COMPLETED_GROUP = "OperationHistory.getRecentlyCompletedGroup";
public static final String QUERY_GET_RECENTLY_COMPLETED_GROUP_ADMIN = "OperationHistory.getRecentlyCompletedGroup_admin";
public static final String QUERY_GET_PARAMETER_CONFIGURATION_IDS = "OperationHistory.getParameterConfigurationIds";
public static final String QUERY_GET_RESULT_CONFIGURATION_IDS = "ResourceOperationHistory.getResultConfigurationIds";
public static final String QUERY_DELETE_BY_HISTORY_IDS = "OperationHistory.deleteByHistoryIds";
public static final String QUERY_DELETE_BY_DEFINITION = "OperationHistory.deleteyByDefinition";
private static final long serialVersionUID = 1L;
@Column(name = "ID")
@GeneratedValue(generator = "RHQ_OPERATION_HISTORY_ID_SEQ", strategy = GenerationType.AUTO)
@Id
private int id;
@Column(name = "JOB_NAME", nullable = false)
private String jobName;
@Column(name = "JOB_GROUP", nullable = false)
private String jobGroup;
@Column(name = "STATUS", nullable = false)
@Enumerated(EnumType.STRING)
private OperationRequestStatus status;
@Column(name = "ERROR_MESSAGE")
private String errorMessage;
@Column(name = "SUBJECT_NAME", nullable = false)
private String subjectName;
@Column(name = "CTIME", nullable = false)
private long createdTime = System.currentTimeMillis();
@Column(name = "STIME", nullable = false)
private long startedTime;
@Column(name = "MTIME", nullable = false)
private long modifiedTime = System.currentTimeMillis();
@JoinColumn(name = "OPERATION_DEF_ID", nullable = false)
@ManyToOne(fetch = FetchType.EAGER)
private OperationDefinition operationDefinition;
@JoinColumn(name = "PARAMETERS_CONFIG_ID", referencedColumnName = "ID", nullable = true)
@OneToOne(cascade = { CascadeType.REMOVE }, fetch = FetchType.LAZY, optional = true)
private Configuration parameters;
protected OperationHistory() {
}
protected OperationHistory(String jobName, String jobGroup, String subjectName,
OperationDefinition operationDefinition, Configuration parameters) {
this.jobName = jobName;
this.jobGroup = jobGroup;
this.subjectName = subjectName;
this.operationDefinition = operationDefinition;
this.parameters = parameters;
this.status = OperationRequestStatus.INPROGRESS;
}
/**
* The single job ID which identifies this specific history item. It can later be parsed via
* {@link HistoryJobId#HistoryJobId(String)}.
*
* @return job ID
*/
public HistoryJobId getJobId() {
return new HistoryJobId(this.jobName, this.jobGroup, this.createdTime);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getJobName() {
return this.jobName;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
public String getJobGroup() {
return this.jobGroup;
}
public void setJobGroup(String jobGroup) {
this.jobGroup = jobGroup;
}
public OperationDefinition getOperationDefinition() {
return operationDefinition;
}
public void setOperationDefinition(OperationDefinition operationDefinition) {
this.operationDefinition = operationDefinition;
}
public Configuration getParameters() {
return parameters;
}
public void setParameters(Configuration parameters) {
this.parameters = parameters;
}
/**
* The status of the operation which indicates that the invocation is either still in progress, or it has completed
* and either succeeded or failed. This may return <code>null</code> if the job hasn't even been triggered yet.
*
* @return the request status
*/
public OperationRequestStatus getStatus() {
return status;
}
public void setStatus(OperationRequestStatus status) {
if (status == null) {
throw new NullPointerException("status==null");
}
this.status = status;
}
/**
* If not <code>null</code>, this is an error message (possibly a full stack trace) to indicate the overall error
* that occurred when the operation failed. This will normally be <code>null</code> unless the
* {@link #getStatus() status} indicates a {@link OperationRequestStatus#FAILURE}.
*
* @return overall error that occurred
*/
public String getErrorMessage() {
return errorMessage;
}
/**
* Calling this method with a non-<code>null</code> error message implies that the request's status is
* {@link OperationRequestStatus#FAILURE}. The inverse is <i>not</i> true - that is, if you set the error message to
* <code>null</code>, the status is left as-is; it will not assume that a <code>null</code> error message means the
* status is successful.
*
* @param errorMessage
*/
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
if (this.errorMessage != null) {
setStatus(OperationRequestStatus.FAILURE);
}
}
/**
* For auditing purposes, this method tells you the username of the person that invoked the operation. This is not a
* relationship to an actual Subject because we want to maintain the audit trail, even if a Subject has been deleted
* from the database.
*
* @return the actual name string of the submitter of the operation invocation
*/
public String getSubjectName() {
return subjectName;
}
/**
* The time this entity was originally created. This may, but will not necessarily, be similar or nearly identical
* to the {@link #getStartedTime()} value. If this is an operation on a single resource, then these two figures
* will be very similar because resource operations are started immediately after the history element is created
* for them. If this is an operation on a resource group, especially a large group, then these two figures might
* diverge considerably because all history elements are created up front, and then the process begins executing
* each in turn.
*
* @return creation time, in epoch milliseconds
* @see #getStartedTime()
*/
public long getCreatedTime() {
return this.createdTime;
}
/**
* The time this entity was last modified. This is the last time the status was updated. If the status has never
* been updated, this will be the {@link #getCreatedTime() created time}.
*
* @return last modified time, in epoch milliseconds
*/
public long getModifiedTime() {
return this.modifiedTime;
}
/**
* This method MUST be called when the corresponding operation is triggered, but before the request is sent down to
* the agent. The started time is used in the calculation of {@link #getDuration()}, which is in turn used by the
* business layer to reason whether an operation has timed out. If this method is never called, and if there are
* any issues executing the corresponding operation, this history element will never time out and will forever stay
* in the {@link OperationRequestStatus#INPROGRESS} state.
*
* @throws IllegalArgumentException if an attempt is made to start this object more than once
* @see #getCreatedTime()
*/
public void setStartedTime() {
if (this.startedTime != 0) {
throw new IllegalArgumentException("Can only start an operation once");
}
this.startedTime = System.currentTimeMillis();
}
/**
* This method MUST be called when the corresponding operation is triggered, but before the request is sent down to
* the agent. The started time is used in the calculation of {@link #getDuration()}, which is in turn used by the
* business layer to reason whether an operation has timed out. If this method is never called, and if there are
* any issues executing the corresponding operation, this history element will never time out and will forever stay
* in the {@link OperationRequestStatus#INPROGRESS} state.
*
* @throws IllegalArgumentException if an attempt is made to start this object more than once
* @see #getCreatedTime()
* @since RHQ 4.9
*/
public void setStartedTime(long startedTime) {
if (this.startedTime != 0) {
throw new IllegalArgumentException("Can only start an operation once");
}
this.startedTime = startedTime;
}
/**
* The time when corresponding operation was started. If the corresponding operation has not yet been started,
* this method will return 0.
* * @return started time, in epoch millis
*/
public long getStartedTime() {
return this.startedTime;
}
/**
* The duration of the operation invocation which simply is the difference between the {@link #getCreatedTime()} and
* the {@link #getModifiedTime()}. If the operation hasn't completed yet, this will be the difference between the
* current time and the created time.
*
* @return the duration of time that the operation took or is taking to complete
*/
public long getDuration() {
// by definition, the duration is 0 if the corresponding operation hasn't begun
if (this.startedTime == 0) {
return 0;
}
long start = this.startedTime;
long end = this.modifiedTime;
if ((status == null) || (status == OperationRequestStatus.INPROGRESS)) {
end = System.currentTimeMillis();
}
return end - start;
}
/*
* there may be some operations whose parameters are sensitive values, such as passwords - do not show them
* as part of the toString. they can still be gotten by explicitly calling getParameters()
*/
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("id=[" + this.id);
buf.append("], job-name=[" + this.jobName);
buf.append("], job-group=[" + this.jobGroup);
buf.append("], status=[" + this.status);
buf.append("], subject-name=[" + this.subjectName);
buf.append("], ctime=[" + new java.util.Date(this.createdTime));
buf.append("], mtime=[" + new java.util.Date(this.modifiedTime));
buf.append("], duration-millis=[" + getDuration());
String err = this.errorMessage;
if ((err != null) && (err.indexOf('\n') > -1)) {
err = err.substring(0, err.indexOf('\n')) + "...";
}
buf.append("], error-message=[" + err);
return buf.toString();
}
@Override
public int hashCode() {
return getJobId().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || !(obj instanceof OperationHistory)) {
return false;
}
final OperationHistory other = (OperationHistory) obj;
return this.getJobId().equals(other.getJobId());
}
@PrePersist
void onPersist() {
this.modifiedTime = System.currentTimeMillis();
}
@PreUpdate
void onUpdate() {
this.modifiedTime = System.currentTimeMillis();
}
}