/*
* The MIT License
*
* Copyright 2012 Sony Ericsson Mobile Communications. All rights reserved.
* Copyright 2012 Sony Mobile Communications AB. 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;
import com.sonyericsson.jenkins.plugins.bfa.graphs.BFAGraph;
import com.sonyericsson.jenkins.plugins.bfa.graphs.BarChart;
import com.sonyericsson.jenkins.plugins.bfa.graphs.GraphFilterBuilder;
import com.sonyericsson.jenkins.plugins.bfa.graphs.GraphType;
import com.sonyericsson.jenkins.plugins.bfa.graphs.PieChart;
import com.sonyericsson.jenkins.plugins.bfa.graphs.TimeSeriesChart;
import com.sonyericsson.jenkins.plugins.bfa.graphs.TimeSeriesUnkownFailuresChart;
import com.sonyericsson.jenkins.plugins.bfa.model.FailureCause;
import com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication;
import com.sonyericsson.jenkins.plugins.bfa.utils.BfaUtils;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Failure;
import hudson.model.Hudson;
import hudson.model.ModelObject;
import hudson.security.Permission;
import hudson.util.Graph;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
/**
* Page for managing the failure causes.
*
* @author Robert Sandell <robert.sandell@sonyericsson.com>
*/
@Extension
public class CauseManagement extends BfaGraphAction {
/**
* Where in the Jenkins name space this action will be.
*
* @see #getUrlName()
*/
public static final String URL_NAME = "failure-cause-management";
/**
* The reserved id for getting a new {@link FailureCause} from {@link #getDynamic(String,
* org.kohsuke.stapler.StaplerRequest, org.kohsuke.stapler.StaplerResponse)}.
*/
public static final String NEW_CAUSE_DYNAMIC_ID = "new";
/**
* The pre-filled name that a new cause gets.
*/
public static final String NEW_CAUSE_NAME = "New...";
/**
* The pre-filled description that a new cause gets.
*/
public static final String NEW_CAUSE_DESCRIPTION = "Description...";
/**
* The request attribute key where error messages are added.
*/
public static final String REQUEST_CAUSE_MANAGEMENT_ERROR = "CauseManagementError";
/**
* Session key for the last removed {@link FailureCause} by the user. Will be removed by the index page when it
* displays it.
*/
public static final String SESSION_REMOVED_FAILURE_CAUSE = "removed-failureCause";
/**
* Title for the page displaying the graphs.
*/
public static final String GRAPH_PAGE_TITLE = "Global statistics";
/**
* Title for graphs with failure causes.
*/
private static final String GRAPH_TITLE_CAUSES = "Failure causes for all nodes";
/**
* Title for graphs with categories.
*/
private static final String GRAPH_TITLE_CATEGORIES = "Failures causes for all nodes grouped by categories";
private static final String GRAPH_TITLE_UNKNOWN_PERCENTAGE = "Unknown failure causes";
private static final String OWNER_URL = "/";
@Override
public String getIconFileName() {
if (Hudson.getInstance().hasPermission(PluginImpl.UPDATE_PERMISSION)
|| Hudson.getInstance().hasPermission(PluginImpl.VIEW_PERMISSION)) {
return PluginImpl.getDefaultIcon();
} else {
return null;
}
}
@Override
public String getDisplayName() {
if (Hudson.getInstance().hasPermission(PluginImpl.UPDATE_PERMISSION)) {
return Messages.CauseManagement_DisplayName();
} else if (Hudson.getInstance().hasPermission(PluginImpl.VIEW_PERMISSION)) {
return Messages.CauseList_DisplayName();
} else {
return null;
}
}
@Override
public String getUrlName() {
return URL_NAME;
}
/**
* Convenience method for calling {@link PluginImpl#getImageUrl(String, String)} from jelly.
*
* @param size the size
* @param name the name
* @return the url.
*
* @see PluginImpl#getImageUrl(String, String)
*/
public String getImageUrl(String size, String name) {
return PluginImpl.getImageUrl(size, name);
}
/**
* Convenience method for {@link com.sonyericsson.jenkins.plugins.bfa.db.KnowledgeBase#getShallowCauses()}.
*
* @return the collection of causes.
*
* @throws Exception if communication fails.
*/
public Iterable<FailureCause> getShallowCauses() throws Exception {
Iterable<FailureCause> returnValue = null;
try {
returnValue = PluginImpl.getInstance().getKnowledgeBase().getShallowCauses();
} catch (Exception e) {
String message = "Could not fetch causes: " + e.getMessage();
setErrorMessage(message);
}
return returnValue;
}
/**
* Sets an error message as an attribute to the current request.
*
* @param message the message to set.
* @see #getErrorMessage(org.kohsuke.stapler.StaplerRequest)
* @see #REQUEST_CAUSE_MANAGEMENT_ERROR
*/
private void setErrorMessage(String message) {
Stapler.getCurrentRequest().setAttribute(REQUEST_CAUSE_MANAGEMENT_ERROR, message);
}
/**
* Convenience method for jelly.
*
* @param request the request where the message might be.
* @return true if there is an error message to display.
*/
public boolean isError(StaplerRequest request) {
return Util.fixEmpty((String)request.getAttribute(REQUEST_CAUSE_MANAGEMENT_ERROR)) != null;
}
/**
* Used for getting the error message to show on the page.
*
* @param request the request where the message might be.
* @return the error message to show.
*/
public String getErrorMessage(StaplerRequest request) {
return (String)request.getAttribute(REQUEST_CAUSE_MANAGEMENT_ERROR);
}
/**
* Dynamic Stapler URL binding. Provides the ability to navigate to a cause via for example:
* <code>/jenkins/failure-cause-management/abf123</code>
*
* @param id the id of the cause of "new" to create a new cause.
* @param request the request
* @param response the response
* @return the cause if found or null.
*
* @throws Exception if communication with the knowledge base failed.
*/
public FailureCause getDynamic(String id, StaplerRequest request, StaplerResponse response) throws Exception {
if (NEW_CAUSE_DYNAMIC_ID.equalsIgnoreCase(id)) {
return new FailureCause(NEW_CAUSE_NAME, NEW_CAUSE_DESCRIPTION);
} else {
return PluginImpl.getInstance().getKnowledgeBase().getCause(id);
}
}
/**
* Web call to remove a {@link FailureCause}. Does a permission check for {@link PluginImpl#REMOVE_PERMISSION}.
*
* @param id the id of the cause to remove.
* @param request the stapler request.
* @param response the stapler response.
* @throws IOException if so during redirect.
*/
public void doRemoveConfirm(@QueryParameter String id, StaplerRequest request, StaplerResponse response)
throws IOException {
Jenkins.getInstance().checkPermission(PluginImpl.REMOVE_PERMISSION);
id = Util.fixEmpty(id);
if (id != null) {
try {
FailureCause cause = PluginImpl.getInstance().getKnowledgeBase().removeCause(id);
if (cause != null) {
request.getSession(true).setAttribute(SESSION_REMOVED_FAILURE_CAUSE, cause);
}
} catch (Exception e) {
//Should we use errorMessage here as well?
throw (Failure)(new Failure(e.getMessage()).initCause(e));
}
}
response.sendRedirect2("./");
}
/**
* The "owner" of this Action. Default this would be {@link hudson.model.Hudson#getInstance()} but if the class is
* included in some build or something we might want to be able to easier change the side panel for example.
*
* @return the holder of the beer.
*/
@Override
public ModelObject getOwner() {
return Hudson.getInstance();
}
/**
* Where to redirect after the form has been saved, probably to the owner.
*
* @return the owner's URL or some place else to redirect the user after save.
*/
protected String getOwnerUrl() {
return OWNER_URL;
}
/**
* Provides a list of all IndicationDescriptors. For Jelly convenience.
*
* @return a list of descriptors.
*
* @see com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication.IndicationDescriptor#getAll()
*/
public ExtensionList<Indication.IndicationDescriptor> getIndicationDescriptors() {
return Indication.IndicationDescriptor.getAll();
}
/**
* The permission related to this action. For Jelly convenience.
*
* @return the permission.
*
* @see PluginImpl#UPDATE_PERMISSION
*/
public Permission getPermission() {
return PluginImpl.UPDATE_PERMISSION;
}
/**
* The permission related to this action. For Jelly convenience.
*
* @return the permission.
*
* @see PluginImpl#UPDATE_PERMISSION
*/
public Permission getRemovePermission() {
return PluginImpl.REMOVE_PERMISSION;
}
/**
* Checks if Jenkins is run from inside a HudsonTestCase. For some reason the buildQueue fails to render when run
* under test but works fine when run with hpi:run. So the jelly file skips the inclusion of the sidepanel if we are
* running under test to work around this problem. The check is done via looking at the class name of {@link
* hudson.model.Hudson#getPluginManager()}.
*
* @return true if we are running under test.
*/
public boolean isUnderTest() {
return "org.jvnet.hudson.test.TestPluginManager".
equals(Hudson.getInstance().getPluginManager().getClass().getName());
}
/**
* Provides the singleton instance of this class that Jenkins has loaded. Throws an IllegalStateException if for
* some reason the action can't be found.
*
* @return the instance.
*/
public static CauseManagement getInstance() {
for (Action action : Hudson.getInstance().getActions()) {
if (action instanceof CauseManagement) {
return (CauseManagement)action;
}
}
throw new IllegalStateException("We seem to not have been initialized!");
}
@Override
public GraphType[] getGraphTypes() {
return new GraphType[] { GraphType.BAR_CHART_CAUSES, GraphType.PIE_CHART_CAUSES,
GraphType.TIME_SERIES_CHART_CAUSES, GraphType.BAR_CHART_CATEGORIES,
GraphType.PIE_CHART_CATEGORIES, GraphType.TIME_SERIES_CHART_CATEGORIES,
GraphType.TIME_SERIES_UNKNOWN_FAILURES, };
}
@Override
public String getGraphsPageTitle() {
return GRAPH_PAGE_TITLE;
}
@Override
public boolean showMasterSwitch() {
return true;
}
@Override
public boolean showGraphDelayText() {
return true;
}
@Override
protected Graph getGraph(GraphType which, Date timePeriod,
boolean hideManAborted, boolean forAllMasters,
Map<String, String> rawReqParams) {
GraphFilterBuilder filter = getDefaultBuilder(hideManAborted,
timePeriod, forAllMasters);
switch (which) {
case BAR_CHART_CAUSES:
return new BarChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT,
null, filter, GRAPH_TITLE_CAUSES, false);
case BAR_CHART_CATEGORIES:
return new BarChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT,
null, filter, GRAPH_TITLE_CATEGORIES, true);
case PIE_CHART_CAUSES:
return new PieChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT,
null, filter, GRAPH_TITLE_CAUSES, false);
case PIE_CHART_CATEGORIES:
return new PieChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT,
null, filter, GRAPH_TITLE_CATEGORIES, true);
case TIME_SERIES_CHART_CAUSES:
return getTimeSeriesChart(false, GRAPH_TITLE_CAUSES, filter,
rawReqParams);
case TIME_SERIES_CHART_CATEGORIES:
return getTimeSeriesChart(true, GRAPH_TITLE_CATEGORIES, filter,
rawReqParams);
case TIME_SERIES_UNKNOWN_FAILURES:
return getTimeSeriesUnknownFailuresChart(GRAPH_TITLE_UNKNOWN_PERCENTAGE, filter, rawReqParams);
default:
break;
}
return null;
}
/**
* Adds time strains to filter, depending on the time frame selected by the user.
* @param filter filter to add time strains for
* @param rawReqParams raw request params
* @return time interval to use for grouping data. This will be {@link Calendar}.HOUR_OF_DAY for the today-view,
* {@link Calendar}.DATE for monthly view and {@link Calendar}.MONTH for max view.
*/
private int addTimeIntervalToFilter(GraphFilterBuilder filter, Map<String, String> rawReqParams) {
String date = rawReqParams.get(URL_PARAM_TIME_PERIOD);
int interval;
Calendar cal = Calendar.getInstance();
if (URL_PARAM_VALUE_TODAY.equals(date)) {
interval = Calendar.HOUR_OF_DAY;
cal.add(Calendar.DAY_OF_YEAR, -1);
} else if (URL_PARAM_VALUE_MONTH.equals(date)) {
interval = Calendar.DATE;
cal.add(Calendar.MONTH, -1);
} else {
interval = Calendar.MONTH;
cal.add(Calendar.YEAR, -BFAGraph.MAX_YEARS_FOR_TIME_GRAPH);
}
filter.setSince(cal.getTime());
return interval;
}
/**
* Get a time series chart that displays unknown failure causes in percent.
* @param title The title of the graph
* @param filter GraphFilterBuilder to specify data to use
* @param rawReqParams A map with the url-parameters from the request
* @return Requested graph
*/
private Graph getTimeSeriesUnknownFailuresChart(String title, GraphFilterBuilder filter,
Map<String, String> rawReqParams) {
int interval = addTimeIntervalToFilter(filter, rawReqParams);
return new TimeSeriesUnkownFailuresChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT, null, filter, interval,
title);
}
/**
* Get a time series chart corresponding to the specified arguments.
* @param byCategories True to group by categories, or false causes
* @param title The title of the graph
* @param filter GraphFilterBuilder to specify data to use
* @param rawReqParams A map with the url-parameters from the request
* @return A time series graph
*/
private Graph getTimeSeriesChart(boolean byCategories, String title, GraphFilterBuilder filter,
Map<String, String> rawReqParams) {
int interval = addTimeIntervalToFilter(filter, rawReqParams);
return new TimeSeriesChart(-1, DEFAULT_GRAPH_WIDTH, DEFAULT_GRAPH_HEIGHT, null, filter, interval, byCategories,
title);
}
/**
* Get a GraphFilterBuilder corresponding to the specified arguments.
* @param hideAborted Hide manually aborted
* @param period The time period
* @param forAllMasters Show for all masters
* @return A GraphFilterBuilder
*/
private GraphFilterBuilder getDefaultBuilder(boolean hideAborted, Date period, boolean forAllMasters) {
GraphFilterBuilder filter = new GraphFilterBuilder();
if (hideAborted) {
filter.setExcludeResult(EXCLUDE_ABORTED);
}
if (!forAllMasters) {
filter.setMasterName(BfaUtils.getMasterName());
}
filter.setSince(period);
return filter;
}
@Override
protected String getGraphCacheId(GraphType whichGraph, String reqTimePeriod,
boolean hideAborted, boolean forAllMasters) {
return getClass().getSimpleName() + whichGraph.getValue() + reqTimePeriod
+ String.valueOf(hideAborted) + String.valueOf(forAllMasters);
}
}