/*
* The MIT License
*
* Copyright 2012 Sony Mobile Communications Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.sonyericsson.jenkins.plugins.bfa.model;
import com.sonyericsson.jenkins.plugins.bfa.CauseManagement;
import com.sonyericsson.jenkins.plugins.bfa.PluginImpl;
import com.sonyericsson.jenkins.plugins.bfa.db.KnowledgeBase;
import com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication;
import hudson.Extension;
import hudson.Util;
import hudson.model.Action;
import hudson.model.AutoCompletionCandidates;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Failure;
import hudson.model.Hudson;
import hudson.model.Job;
import hudson.model.User;
import hudson.util.FormValidation;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import net.vz.mongodb.jackson.Id;
import net.vz.mongodb.jackson.ObjectId;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonIgnoreType;
import org.codehaus.jackson.annotate.JsonProperty;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* FailureCause of a build.
*
* @author Tomas Westling <thomas.westling@sonyericsson.com>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class FailureCause implements Serializable, Action, Describable<FailureCause> {
private static final Logger logger = Logger.getLogger(FailureCause.class.getName());
private String id;
private String name;
private String description;
private String comment;
private Date lastOccurred;
private List<String> categories;
private List<Indication> indications;
private List<FailureCauseModification> modifications;
/**
* Standard data bound constructor.
*
* @param id the id.
* @param name the name of this FailureCause.
* @param description the description of this FailureCause.
* @param comment the comment of this FailureCause.
* @param lastOccurred the time at which this FailureCause last occurred.
* @param categories the categories of this FailureCause.
* @param indications the list of indications
* @param modifications the modification history of this FailureCause.
*/
@DataBoundConstructor
public FailureCause(String id, String name, String description, String comment,
Date lastOccurred, String categories, List<Indication> indications,
List<FailureCauseModification> modifications) {
this(id, name, description, comment, lastOccurred, Arrays.<String>asList(Util.tokenize(categories)),
indications, modifications);
}
/**
* JSON constructor.
*
* @param id the id.
* @param name the name of this FailureCause.
* @param description the description of this FailureCause.
* @param comment the comment of this FailureCause.
* @param lastOccurred the last time this FailureCause occurred.
* @param categories the categories of this FailureCause.
* @param indications the list of indications
* @param modifications the modification history of this FailureCause.
*/
@JsonCreator
public FailureCause(@Id @ObjectId String id, @JsonProperty("name") String name, @JsonProperty("description")
String description, @JsonProperty("comment") String comment, @JsonProperty("occurred") Date lastOccurred,
@JsonProperty("categories") List<String> categories,
@JsonProperty("indications") List<Indication> indications,
@JsonProperty("modifications") List<FailureCauseModification> modifications) {
this.id = Util.fixEmpty(id);
this.name = name;
this.description = description;
this.comment = comment;
if (lastOccurred == null) {
this.lastOccurred = null;
} else {
this.lastOccurred = (Date)lastOccurred.clone();
}
this.categories = categories;
this.indications = indications;
if (this.indications == null) {
this.indications = new LinkedList<Indication>();
}
this.modifications = modifications;
if (this.modifications == null) {
this.modifications = new LinkedList<FailureCauseModification>();
}
}
/**
* Standard constructor.
*
* @param name the name of this FailureCause.
* @param description the description of this FailureCause.
*/
public FailureCause(String name, String description) {
this(null, name, description, "", null, "", null, null);
}
/**
* Standard constructor.
*
* @param name the name of this FailureCause.
* @param description the description of this FailureCause.
* @param comment the comment for this FailureCause.
*/
public FailureCause(String name, String description, String comment) {
this(null, name, description, comment, null, "", null, null);
}
/**
* Default constructor. <strong>Do not use this unless you are a serializer.</strong>
*/
public FailureCause() {
}
/**
* Validates this FailureCause. Checks for: {@link #doCheckName(String)}, {@link #doCheckDescription(String)},
* Indications.size > 0. and {@link com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication#validate()}.
*
* @param newName the name to validate
* @param newDescription the description
* @param newIndications the list of indications
* @return {@link hudson.util.FormValidation#ok()} if everything is fine.
*/
public FormValidation validate(String newName,
String newDescription,
List<Indication> newIndications) {
FormValidation nameVal = doCheckName(newName);
if (nameVal.kind != FormValidation.Kind.OK) {
return nameVal;
}
FormValidation descriptionVal = doCheckDescription(newDescription);
if (descriptionVal.kind != FormValidation.Kind.OK) {
return descriptionVal;
}
if (newIndications == null || newIndications.isEmpty()) {
return FormValidation.error("Need at least one indication for " + newName);
}
for (Indication indication : newIndications) {
FormValidation validation = indication.validate();
if (validation.kind != FormValidation.Kind.OK) {
return validation;
}
}
return FormValidation.ok();
}
/**
* Form validation for {@link #description}. Checks for not empty and not "Description..."
*
* @param value the form value.
* @return {@link hudson.util.FormValidation#ok()} if everything is well.
*/
public FormValidation doCheckDescription(@QueryParameter final String value) {
if (Util.fixEmpty(value) == null) {
return FormValidation.error("You should provide a description.");
}
if (CauseManagement.NEW_CAUSE_DESCRIPTION.equalsIgnoreCase(value.trim())) {
return FormValidation.error("Bad description.");
}
return FormValidation.ok();
}
/**
* Form validation for {@link #name}. Checks for not empty, not "New...", {@link Jenkins#checkGoodName(String)} and
* that it is unique based on the cache of existing causes.
*
* @param value the form value.
* @return {@link hudson.util.FormValidation#ok()} if everything is well.
*/
public FormValidation doCheckName(@QueryParameter final String value) {
if (Util.fixEmpty(value) == null) {
return FormValidation.error("You must provide a name for the failure cause!");
}
if (CauseManagement.NEW_CAUSE_NAME.equalsIgnoreCase(value)) {
return FormValidation.error("Reserved name!");
}
try {
Jenkins.checkGoodName(value);
} catch (Failure failure) {
return FormValidation.error(failure, failure.getMessage());
}
//Use the cache it's hopefully good enough
try {
for (FailureCause other : PluginImpl.getInstance().getKnowledgeBase().getCauses()) {
if ((id == null || !id.equals(other.getId())) && value.equals(other.getName())) {
return FormValidation.error("There is another cause with that name.");
}
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to get causes list to evaluate name! ", e);
}
return FormValidation.ok();
}
/**
* The form submission handler. Takes the input form and stores the data. Called by Stapler.
*
* @param request the request.
* @param response the response
* @throws Exception if it fails to save to the knowledge base or a validation error occurs.
*/
public synchronized void doConfigSubmit(StaplerRequest request, StaplerResponse response)
throws Exception {
logger.entering(getClass().getName(), "doConfigSubmit");
Jenkins.getInstance().checkPermission(PluginImpl.UPDATE_PERMISSION);
JSONObject form = request.getSubmittedForm();
String newId = form.getString("id");
newId = Util.fixEmpty(newId);
String oldId = Util.fixEmpty(id);
//Just some paranoid checks
if (newId != null) {
if (oldId != null && !newId.equals(oldId)) {
throw new Failure("Attempt at changing the wrong cause! Expected [" + id + "] but got [" + newId + "]");
} else if (oldId == null) {
throw new Failure("Attempt at setting id of new cause!");
}
} else if (oldId != null) {
throw new Failure("Clone attempt of cause [" + id + "]");
}
String newName = form.getString("name");
String newDescription = form.getString("description");
String newComment = form.getString("comment");
String jsonCategories = form.optString("categories");
if (Util.fixEmpty(jsonCategories) != null) {
this.categories = Arrays.asList(Util.tokenize(jsonCategories));
} else {
this.categories = null;
}
Object jsonIndications = form.opt("indications");
if (jsonIndications == null) {
throw new Failure("You need to provide at least one indication!");
}
List<Indication> newIndications = request.bindJSONToList(Indication.class, jsonIndications);
FormValidation validation = validate(newName, newDescription, newIndications);
if (validation.kind != FormValidation.Kind.OK) {
throw validation;
}
this.name = newName;
this.description = newDescription;
this.comment = newComment;
this.indications = newIndications;
String user = null;
try {
user = User.current().getId();
} catch (NullPointerException npe) {
logger.log(Level.INFO,
"Failed to get user for Failure Cause modification");
}
this.modifications.add(0, new FailureCauseModification(user, new Date()));
if (newId == null) {
PluginImpl.getInstance().getKnowledgeBase().addCause(this);
} else {
PluginImpl.getInstance().getKnowledgeBase().saveCause(this);
}
response.sendRedirect2("../");
}
/**
* Adds an indication to the list.
*
* @param indication the indication to add.
*/
public void addIndication(Indication indication) {
if (indications == null) {
indications = new LinkedList<Indication>();
}
indications.add(indication);
}
/**
* The id.
*
* @return the id.
*/
@Id
@ObjectId
public String getId() {
return id;
}
/**
* The id.
*
* @param id the id.
*/
@Id
@ObjectId
public void setId(String id) {
this.id = id;
}
/**
* Getter for the name.
*
* @return the name.
*/
public String getName() {
return name;
}
/**
* Getter for the description.
*
* @return the description.
*/
public String getDescription() {
return description;
}
/**
* Getter for the comment.
*
* @return the comment.
*/
public String getComment() {
return comment;
}
/**
* Getter for the last occurrence.
*
* @return the last occurrence.
*/
public Date getLastOccurred() {
if (lastOccurred != null) {
return (Date)lastOccurred.clone();
} else {
return null;
}
}
/**
* Initiates the last occurrence if it's not already initiated
* and then returns the date of last modification.
* @return the last occurrence.
*/
@JsonIgnore
public Date getAndInitiateLastOccurred() {
if (lastOccurred == null && id != null) {
loadLastOccurred();
}
if (lastOccurred != null) {
return (Date)lastOccurred.clone();
} else {
return null;
}
}
/**
* Setter for the last occurrence.
*
* @param lastOccurred the occurrence to set.
*/
public void setLastOccurred(Date lastOccurred) {
if (lastOccurred == null) {
this.lastOccurred = null;
} else {
this.lastOccurred = (Date)lastOccurred.clone();
}
}
/**
* Getter for the list of modifications.
*
* @return the modifications.
*/
public List<FailureCauseModification> getModifications() {
return modifications;
}
/**
* Initiates the list of modifications if it's not already initiated
* and then returns the list.
* @return list of modifications
*/
@JsonIgnore
public List<FailureCauseModification> getAndInitiateModifications() {
if ((modifications == null || modifications.isEmpty())
&& id != null) {
initModifications();
}
return modifications;
}
/**
* Getter for the categories.
*
* @return the categories.
*/
public List<String> getCategories() {
return categories;
}
/**
* Returns the categories as a String, used for the view.
*
* @return the categories as a String.
*/
@JsonIgnore
public String getCategoriesAsString() {
if (categories == null || categories.isEmpty()) {
return null;
}
StringBuilder builder = new StringBuilder();
for (String item : categories) {
if (builder.length() > 0) {
builder.append(" ");
}
builder.append(item);
}
return builder.toString();
}
/**
* Helper method for initializing the list of FailureCauseModifications done to this FailureCause.
*/
private void initModifications() {
if (this.modifications == null) {
this.modifications = new LinkedList<FailureCauseModification>();
}
KnowledgeBase kb = PluginImpl.getInstance().getKnowledgeBase();
Date creationDate = kb.getCreationDateForCause(id);
FailureCauseModification creation = new FailureCauseModification(null, creationDate);
this.modifications.add(creation);
FailureCause originalCause = null;
try {
originalCause = kb.getCause(this.id);
} catch (Exception e) {
logger.log(Level.WARNING, "Got exception when loading the original FailureCause");
// Handled in finally-clause
} finally {
if (originalCause == null) {
logger.warning("Original FailureCause was null");
return;
}
}
if (originalCause.modifications == null) {
originalCause.modifications = new LinkedList<FailureCauseModification>();
}
originalCause.modifications.add(creation);
try {
kb.saveCause(originalCause);
} catch (Exception e) {
logger.warning("Failed saving failure cause modification to knowledgeBase");
}
}
/**
* Gets the latest {@link FailureCauseModification} of this FailureCause.
*
* @return the latest modification
*/
@JsonIgnore
public FailureCauseModification getLatestModification() {
List<FailureCauseModification> mods = getAndInitiateModifications();
if (mods != null && !mods.isEmpty()) {
FailureCauseModification latestMod = mods.get(0);
if (latestMod.getTime().getTime() > 0) {
return latestMod;
}
}
return null;
}
/**
* If we're missing information about when this FailureCause last occurred,
* try to find an occurrence in the knowledgeBase.
* If none is found, set the lastOccurred-attribute to the unix epoch, which symbolizes 'Never'.
*/
private void loadLastOccurred() {
this.lastOccurred = PluginImpl.getInstance().getKnowledgeBase().getLatestFailureForCause(this.id);
if (lastOccurred == null) {
lastOccurred = new Date(0);
}
try {
FailureCause originalCause = PluginImpl.getInstance().getKnowledgeBase().getCause(this.id);
originalCause.setLastOccurred(lastOccurred);
PluginImpl.getInstance().getKnowledgeBase().saveCause(originalCause);
} catch (Exception e) {
logger.log(Level.WARNING, "Failed updating lastOccurred", e);
}
}
/**
* Setter for the categories.
*
* @param categories the categories.
*/
public void setCategories(List<String> categories) {
this.categories = categories;
}
/**
* Getter for the list of indications.
*
* @return the list.
*/
public List<Indication> getIndications() {
if (indications == null) {
indications = new LinkedList<Indication>();
}
return indications;
}
//CS IGNORE JavadocMethod FOR NEXT 8 LINES. REASON: The exception can be thrown.
/**
* Finds the {@link CauseManagement} ancestor of the {@link Stapler#getCurrentRequest() current request}.
*
* @return the management action or a derivative of it, or null if no management action is found.
* @throws IllegalStateException if no ancestor is found.
*/
@JsonIgnore
public CauseManagement getAncestorCauseManagement() {
StaplerRequest currentRequest = Stapler.getCurrentRequest();
if (currentRequest == null) {
return null;
}
CauseManagement ancestorObject = currentRequest.findAncestorObject(CauseManagement.class);
if (ancestorObject == null) {
return null;
}
return ancestorObject;
}
@Override
@JsonIgnore
public String getIconFileName() {
return PluginImpl.getDefaultIcon();
}
@Override
@JsonIgnore
public String getDisplayName() {
return name;
}
@Override
@JsonIgnore
public String getUrlName() {
return id;
}
@Override
public FailureCauseDescriptor getDescriptor() {
return Jenkins.getInstance().getDescriptorByType(FailureCauseDescriptor.class);
}
/**
* Descriptor is only used for auto completion of categories.
*/
@Extension
@JsonIgnoreType
public static final class FailureCauseDescriptor extends Descriptor<FailureCause> {
/**
* The name of a session attribute which stores the url to the last failed build of the project from
* whose page the Failure Cause Management page was entered.
*/
private static final String LAST_FAILED_BUILD_URL_SESSION_ATTRIBUTE_NAME = "BFA_LAST_FAILED_BUILD_URL";
/**
* @return the URL to the last failed build of the project from whose page the Failure Cause Management
* page was entered.
*/
public String getLastFailedBuildUrl() {
StaplerRequest staplerRequest = Stapler.getCurrentRequest();
if (staplerRequest != null) {
String answer = (String)staplerRequest.getSession(true).
getAttribute(LAST_FAILED_BUILD_URL_SESSION_ATTRIBUTE_NAME);
if (answer != null) {
return answer;
}
}
return "";
}
/**
* Set the URL of the last failed build of the project from whose page the Failure Cause Management
* page was entered.
*/
public void setLastFailedBuildUrl() {
StaplerRequest staplerRequest = Stapler.getCurrentRequest();
if (staplerRequest != null) {
Job project = staplerRequest.findAncestorObject(Job.class);
if (project != null && project.getLastFailedBuild() != null) {
staplerRequest.getSession(true).setAttribute(LAST_FAILED_BUILD_URL_SESSION_ATTRIBUTE_NAME,
Hudson.getInstance().getRootUrl() + project.getLastFailedBuild().getUrl());
} else {
staplerRequest.getSession(true).setAttribute(LAST_FAILED_BUILD_URL_SESSION_ATTRIBUTE_NAME, "");
}
}
}
@Override
public String getDisplayName() {
return null;
}
/**
* Does the auto completion for categories, matching with any category already present in the knowledge base.
*
* @param value the input value.
* @return the AutoCompletionCandidates.
*/
public AutoCompletionCandidates doAutoCompleteCategories(@QueryParameter String value) {
List<String> categories;
try {
categories = PluginImpl.getInstance().getKnowledgeBase().getCategories();
} catch (Exception e) {
logger.log(Level.WARNING, "Could not get the categories for autocompletion", e);
return null;
}
AutoCompletionCandidates candidates = new AutoCompletionCandidates();
if (categories == null) {
return candidates;
}
for (String category : categories) {
if (category.toLowerCase().startsWith(value.toLowerCase())) {
candidates.add(category);
}
}
return candidates;
}
}
}