/* * 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.model.indication; import com.sonyericsson.jenkins.plugins.bfa.Messages; import com.sonyericsson.jenkins.plugins.bfa.model.BuildLogFailureReader; import com.sonyericsson.jenkins.plugins.bfa.model.FailureReader; import hudson.Extension; import hudson.matrix.MatrixConfiguration; import hudson.matrix.MatrixProject; import hudson.model.Hudson; import hudson.model.ItemGroup; import hudson.model.Job; import hudson.model.Run; import hudson.util.FormValidation; import jenkins.model.Jenkins; import org.codehaus.jackson.annotate.JsonProperty; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Indication that parses the build log file for a pattern. * * @author Tomas Westling <thomas.westling@sonyericsson.com> */ public class BuildLogIndication extends Indication { private static final long serialVersionUID = -2889792693081908532L; private transient Pattern compiled = null; /** * Standard constructor. * * @param pattern the string value to search for. */ @DataBoundConstructor public BuildLogIndication(@JsonProperty("pattern") String pattern) { super(pattern); } /** * Default constructor. * <strong>Do not use this unless you are a serializer.</strong> */ public BuildLogIndication() { } @Override public FailureReader getReader() { return new BuildLogFailureReader(this); } @Override public Pattern getPattern() { if (compiled == null) { compiled = Pattern.compile(getUserProvidedExpression()); } return compiled; } @Override public IndicationDescriptor getDescriptor() { return Hudson.getInstance().getDescriptorByType(BuildLogIndicationDescriptor.class); } /** * The descriptor. */ @Extension public static class BuildLogIndicationDescriptor extends IndicationDescriptor { /** * A pattern matched by all Jenkins job URL:s. */ private static final Pattern URL_PATTERN = Pattern.compile("^(.*/)([^/]+)/([^/]+)/([^/]+)/?$"); /** * The number of groups in URL_PATTERN. */ private static final int NUM_OF_URL_PARTS = 4; /** * An identifier for a non-numeric build for a given project, * like "last build" and "last failed build". */ public enum StringBuildId { /** * Last build. */ LAST_BUILD("lastBuild") { /** * @param project a project. * @return the build of the given project based on this StringBuildId. * * @see StringBuildId#getBuild(hudson.model.Job) */ @Override public Run getBuild(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project) { return project.getLastBuild(); } }, /** * Last failed build. */ LAST_FAILED_BUILD("lastFailedBuild") { /** * @param project a project. * @return the build of the given project based on this StringBuildId. * * @see StringBuildId#getBuild(hudson.model.Job) */ @Override public Run getBuild(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project) { return project.getLastFailedBuild(); } }, /** * Last unsuccessful build. */ LAST_UNSUCCESSFUL_BUILD("lastUnsuccessfulBuild") { /** * @param project a project. * @return the build of the given project based on this StringBuildId. * * @see StringBuildId#getBuild(hudson.model.Job) */ @Override public Run getBuild(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project) { return project.getLastUnsuccessfulBuild(); } }, /** * Last successful build. */ LAST_SUCCESSFUL_BUILD("lastSuccessfulBuild") { /** * @param project a project. * @return the build of the given project based on this StringBuildId. * * @see StringBuildId#getBuild(hudson.model.Job) */ @Override public Run getBuild(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project) { return project.getLastSuccessfulBuild(); } }; /** * The name of this StringBuildId. */ private final String name; /** * Private constructor. * * @param name the name of this StringBuildId. */ private StringBuildId(String name) { this.name = name; } /** * Returns the name of this StringBuildId. * * @return the name of this StringBuildId. */ public String getName() { return name; } /** * Returns a StringBuildId based on a given string. * * @param str a string. * @return the StringBuildId whose name equals str, if * such a StringBuildId exists; otherwise, return null. */ public static StringBuildId fromString(String str) { if (str != null) { for (StringBuildId stringBuildId : values()) { if (str.equals(stringBuildId.getName())) { return stringBuildId; } } } return null; } /** * Returns a build of a given project based on this StringBuildId. * * @param project a project. * @return the build of the given project based on this StringBuildId. */ public abstract Run getBuild(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project); } @Override public String getDisplayName() { return Messages.BuildLogIndication_DisplayName(); } /** * Tests if a text matches a pattern. * @param testPattern a pattern. * @param testText a text. * @param textSourceIsUrl a boolean indicating whether testText is a URL containing the text to be matched * against pattern or a text that should be matched directly against pattern. * @return {@link FormValidation#ok(java.lang.String) } if the pattern is valid and * the string matches the pattern, * {@link FormValidation#warning(java.lang.String) } if the pattern is valid and * the string does not match the pattern, * {@link FormValidation#error(java.lang.String) } otherwise. */ public FormValidation doMatchText( @QueryParameter("pattern") final String testPattern, @QueryParameter("testText") String testText, @QueryParameter("textSourceIsUrl") final boolean textSourceIsUrl) { if (textSourceIsUrl) { testText = testText.replaceAll("/\\./", "/"); Matcher urlMatcher = URL_PATTERN.matcher(testText); if (urlMatcher.matches()) { String[] urlParts = new String[NUM_OF_URL_PARTS]; for (int i = 0; i < urlParts.length; i++) { urlParts[i] = urlMatcher.group(i + 1); } Run build = null; ItemGroup getItemInstance; if (urlParts[0].split("/job/").length > 1) { /* * We matched a folders job. Let's get the jobs up to the part were the next * iteration can be continued from */ String fullFolderName = ""; /* The interestingJobParts string created below is meant to discard everything * that comes before the first '/job' occurrent which is either nothing or the * prefix from where jenkins is served, ie: http://localhost/jenkins/job/<job>/<buildNumber> */ String[] interestingJobParts = urlParts[0].split("/job/", 2); String[] jobParts = interestingJobParts[interestingJobParts.length - 1].split("/job/"); for (String part: jobParts) { fullFolderName += "/" + part; } getItemInstance = (ItemGroup)Jenkins.getInstance().getItemByFullName(fullFolderName); } else { getItemInstance = (ItemGroup)Jenkins.getInstance(); } /* Find out which of the following url types testText matches, if any, and assign to build accordingly. The url types are checked in the given order. Type 1: .../<job>/<buildNumber>/ Type 2: .../<job>/<matrixInfo>/<buildNumber>/ Type 3: .../<job>/<buildNumber>/<matrixInfo>/ */ if (getItemInstance.getItem(urlParts[2]) instanceof Job && isValidBuildId(urlParts[3])) { Job project = (Job)getItemInstance.getItem(urlParts[2]); build = getBuildById(project, urlParts[3]); } else if (getItemInstance.getItem(urlParts[1]) instanceof MatrixProject && isValidBuildId(urlParts[3])) { MatrixProject project = (MatrixProject)getItemInstance.getItem(urlParts[1]); MatrixConfiguration configuration = project.getItem(urlParts[2]); build = getBuildById(configuration, urlParts[3]); } else if (getItemInstance.getItem(urlParts[1]) instanceof MatrixProject && isValidBuildId(urlParts[2])) { MatrixProject matrixProject = (MatrixProject)getItemInstance.getItem(urlParts[1]); MatrixConfiguration configuration = matrixProject.getItem(urlParts[3]); build = getBuildById(configuration, urlParts[2]); } if (build != null) { try { final FailureReader failureReader = getFailureReader(testPattern); final FoundIndication foundIndication = failureReader.scan(build); if (foundIndication == null) { return FormValidation.warning(Messages.StringDoesNotMatchPattern()); } return FormValidation.okWithMarkup(foundIndication.getFirstMatchingLine()); } catch (IOException e) { return FormValidation.error(Messages.FailedToScanFile_Error()); } } } return FormValidation.error(Messages.InvalidURL_Error()); } else { try { if (testText.matches(testPattern)) { return FormValidation.ok(Messages.StringMatchesPattern()); } return FormValidation.warning(Messages.StringDoesNotMatchPattern()); } catch (PatternSyntaxException e) { return FormValidation.error(Messages.InvalidPattern_Error()); } } } /** * Return whether a given string is a valid build id. * * @param id a string. * @return true if the string is a valid build id; false otherwise. */ private boolean isValidBuildId(String id) { return id.matches("\\d+") || StringBuildId.fromString(id) != null; } /** * Return the build defined by a given project and id. * * @param project a project. * @param id a build id. * @return the build defined by the given project and id, or null if no build can be * found for the given project and id. */ private Run getBuildById(Job<? extends Job<?, ?>, ? extends Run<?, ?>> project, String id) { if (id.matches("\\d+")) { return project.getBuildByNumber(Integer.parseInt(id)); } else { StringBuildId stringBuildId = StringBuildId.fromString(id); if (stringBuildId != null) { return stringBuildId.getBuild(project); } return null; } } /** * @param testPattern the test pattern for the indication passed to the failure reader * @return the failure reader corresponding to this descriptor */ protected FailureReader getFailureReader(final String testPattern) { return new BuildLogFailureReader(new BuildLogIndication(testPattern)); } } }