package hudson.plugins.reviewboard; import hudson.Extension; import hudson.model.AbstractProject; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Publisher; import hudson.util.FormValidation; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.servlet.ServletException; import net.sf.json.JSONObject; import org.apache.commons.httpclient.URIException; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import com.google.common.collect.ImmutableSet; import com.twelvegm.hudson.plugin.reviewboard.ReviewboardHttpAPI; /** * Descriptor for {@link ReviewboardPublisher}. Used as a singleton. * The class is marked as public so that it can be accessed from views. * * @author Ryan Shelley * @version 1.0 */ @Extension public final class ReviewboardDescriptorImpl extends BuildStepDescriptor<Publisher> { private String url; private String username; private String password; private String cmdPath; private transient Set<String> reviewboardUsers = null; private transient Set<String> reviewboardGroups = null; private transient ReviewboardHttpAPI rbApi = null; public ReviewboardDescriptorImpl(){ super(ReviewboardPublisher.class); load(); this.populateReviewboardLists(); } /** * Loads all users and groups from Reviewboard for use with dropdown population * and validation checks. Sets are immutable. */ private void populateReviewboardLists(){ if(this.isPluginConfigured()){ try { reviewboardGroups = Collections.unmodifiableSet(this.getReviewboardAPI().getGroups("")); reviewboardUsers = Collections.unmodifiableSet(this.getReviewboardAPI().getReviewers("")); } catch (URIException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NullPointerException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * Validates the regular expression supplied is a valid pattern for the external ID * as set on the Build's configuration page. * * Example description: FMYHUD-123 fixes issues with build * Example expression : FMYHUD-[0-9]+ * * The result will be "FMYHUD-123" is extracted and used as an external key to map future changes * to existing review requests (so if someone submits several change requests with the same external * ID, the original review request is updated instead of new review requests being created). * * @param keyRegEx regular expression matching the external ID to extract from the change description * @return FormValidation.ok if the regular expression is valid, FormValidation.error if not * @throws IOException * @throws ServletException */ public FormValidation doCheckKeyRegEx(@QueryParameter String keyRegEx) throws IOException, ServletException { if(keyRegEx != null && !keyRegEx.isEmpty()){ try{ Pattern.compile(keyRegEx); return FormValidation.okWithMarkup("<span style=\"color:green;\">Regular Expression is valid.</span>"); }catch(PatternSyntaxException pse){ return FormValidation.error("Regular Expression is not valid."); } } return FormValidation.ok(); } /** * Validates that the command supplied exists and can be executed. Searches the * path environment for the executable. Complete path is not necessary. * * @param cmdPath command to execute post-review, and is often just "post-review.exe" * @return FormValidation.ok if the command is found, FormValidation.error if not * @throws IOException * @throws ServletException */ public FormValidation doCheckCmdPath(@QueryParameter String cmdPath) throws IOException, ServletException { if(this.url != null && !this.url.isEmpty() && cmdPath != null && !cmdPath.isEmpty()){ return FormValidation.validateExecutable(cmdPath); }else{ return FormValidation.error("Path to post-review must be supplied."); } } /** * Validates that the number of days before a review is considered stale is greater * than or equal to -1. -1 = never * * @param cmdPath command to execute post-review, and is often just "post-review.exe" * @return FormValidation.ok if the command is found, FormValidation.error if not * @throws IOException * @throws ServletException */ public FormValidation doCheckDaysBeforeStaleReview(@QueryParameter Integer daysBeforeStaleReview) throws IOException, ServletException { if(daysBeforeStaleReview != null && daysBeforeStaleReview >= -1){ return FormValidation.ok(); }else if (daysBeforeStaleReview == null){ return FormValidation.ok(); }else{ return FormValidation.error("Cannot be less than -1"); } } /** * Validates that the Reviewboard URL supplied is available. * * @param url URL to reviewboard instance * @return FormValidation.ok if the URL was available, FormValidation.error if not * @throws IOException * @throws ServletException */ public FormValidation doCheckUrl(@QueryParameter String url) throws IOException, ServletException { if(url != null && !url.isEmpty()){ try { if(isValidURL(url)) return FormValidation.ok(); else return FormValidation.error("Connection to Reviewboard failed."); } catch(MalformedURLException mue){ return FormValidation.error("Malformed URL."); } catch (URISyntaxException use) { return FormValidation.error("Invalid URL syntax."); } catch (UnknownHostException uhe) { return FormValidation.error("Unknown host."); } catch (Exception e){ return FormValidation.error("Connection to Reviewboard failed. " + e.getMessage()); } } return FormValidation.ok(); } /** * Validates that the reviewers entered match existing Reviewboard users. * * @param defaultReviewers Comma-delimited list of reviewers to check * @return FormValidation.ok if field is empty or users all exist, otherwise FormValidation.error * @throws IOException * @throws ServletException */ public FormValidation doCheckDefaultReviewers(@QueryParameter String defaultReviewers) throws IOException, ServletException { if(defaultReviewers != null && defaultReviewers.trim().length() > 0){ String[] userArray = defaultReviewers.split(","); for(String user: userArray){ user = user.trim(); Set<String> users = this.getReviewboardAPI().getReviewers(user); if(users == null || users.size() == 0) return FormValidation.error("Reviewer \"" + user + "\" was not found in Reviewboard. Usernames are case-sensitive."); else if(users.size() > 1) return FormValidation.error("Reviewer \"" + user + "\" matched more than one user in Reviewboard. Matched values: " + users.toString()); else if(users.contains(user)) continue; else return FormValidation.error("Reviewer \"" + user + "\" did not match any Reviewboard users. Usernames are case-sensitive. Did you mean " + users.toArray()[0] + "?"); } } return FormValidation.ok(); } /** * Validates that the review groups entered match existing Reviewboard groups. * * @param defaultReviewGroups Comma-delimited list of groups to check * @return FormValidation.ok if field is empty or groups all exist, otherwise FormValidation.error * @throws IOException * @throws ServletException */ public FormValidation doCheckDefaultReviewGroups(@QueryParameter String defaultReviewGroups) throws IOException, ServletException { if(defaultReviewGroups != null && defaultReviewGroups.trim().length() > 0){ String[] groupArray = defaultReviewGroups.split(","); for(String group: groupArray){ group = group.trim(); Set<String> groups = this.getReviewboardAPI().getGroups(group); if(groups == null || groups.size() == 0) return FormValidation.error("Group \"" + group + "\" was not found in Reviewboard."); else if(groups.size() > 1) return FormValidation.error("Group \"" + group + "\" matched more than one group in Reviewboard. Matched values: " + groups.toString()); else if(groups.contains(group)) continue; else return FormValidation.error("Group \"" + group + "\" did not match any Reviewboard groups. Did you mean " + groups.toArray()[0] + "?"); } } return FormValidation.ok(); } public FormValidation doCheckForceUpdateOverride(@QueryParameter boolean forceUpdateOverride, @QueryParameter boolean skipUnflaggedChanges){ if(forceUpdateOverride && !skipUnflaggedChanges) return FormValidation.warning("This is only active when \"Skip unflagged changes\" is enabled. This setting will be ignored."); return FormValidation.ok(); } public FormValidation doCheckSkipUnflaggedChanges(@QueryParameter boolean forceUpdateOverride, @QueryParameter boolean skipUnflaggedChanges){ if(forceUpdateOverride && !skipUnflaggedChanges) return FormValidation.warning("The option \"Require RB_UPDATE\" will be ignored since it requires that this option be enabled to be active"); return FormValidation.ok(); } public boolean isApplicable(Class<? extends AbstractProject> aClass) { // indicates that this builder can be used with all kinds of project types return true; } /** * This human readable name is used in the configuration screen. * * @return name of plugin */ public String getDisplayName() { return "Review Board Publisher"; } /** * Configures the Reviewboard plugin with parameters supplied on the Global configuration page of Hudson. * * @param req Form submission request * @param o JSON object containing values from global configuration */ @Override public boolean configure(StaplerRequest req, JSONObject o) throws FormException { // to persist global configuration information, // set that to properties and call save(). url = o.getString("url"); username = o.getString("username"); password = o.getString("password"); cmdPath = o.getString("cmdPath"); try { rbApi = new ReviewboardHttpAPI(username, password, url); } catch (URIException e) { throw new FormException(e, e.getMessage()); } catch (NullPointerException e) { throw new FormException(e, e.getMessage()); } // Save to global config file save(); return super.configure(req,o); } /** * Returns the reviewboard URL * * @return reviewboard URL */ public String getUrl() { return url; } /** * Returns the reviewboard username * * @return reviewboard username */ public String getUsername() { return username; } /** * Returns the reviewboard password * * @return reviewboard password */ public String getPassword() { return password; } /** * Returns the reviewboard command path * * @return reviewboard command path */ public String getCmdPath() { return cmdPath; } public Set<String> getReviewboardGroups(){ return this.reviewboardGroups; } public Set<String> getReviewboardUsers(){ return this.reviewboardUsers; } /** * Returns a configured Reviewboard API * * @return Configured and ready-to-use Reviewboard API, or null if an error occurred creating it * @throws URIException * @throws NullPointerException */ protected ReviewboardHttpAPI getReviewboardAPI() throws URIException, NullPointerException{ if(rbApi == null) rbApi = new ReviewboardHttpAPI(this.username, this.password, this.url); return rbApi; } /** * Validates a URL is available and responds with a 200 error code. * * @param url to check * @return true if response code is >= 200 and < 300, otherwise false * @throws URISyntaxException * @throws MalformedURLException * @throws IOException */ protected boolean isValidURL(String url) throws URISyntaxException, MalformedURLException, IOException{ URI uri = new URI(url); HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection(); try{ return (conn.getResponseCode() >= 200 && conn.getResponseCode() < 300); }finally{ if(conn != null) conn.disconnect(); } } /** * Validates that the saved URL is available. This is executed before the build tries to * create a reviewboard request to ensure reviewboard is available. * * @return true if the URL is available, false otherwise */ protected boolean isSavedURLValid(){ try { return this.isValidURL(this.url); } catch (URISyntaxException e) { return false; } catch (IOException e) { return false; } } /** * Validates that the command path to post-review is available and executable. * * @return true if available, false otherwise */ protected boolean isSavedCommandPathValid(){ return FormValidation.validateExecutable(cmdPath).kind.compareTo(FormValidation.Kind.OK) == 0; } /** * Validates that the plugin is properly configured and reviewboard is available. * This is called before the plugin is executed after a build to ensure that * it can be executed properly. * * @return true if the plugin is properly configured and reviewboard is available, false otherwise */ protected boolean isPluginConfigured(){ return (isSavedURLValid() && isSavedCommandPathValid()); } }