/* * 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(); } }