/* * Copyright (C) Jakub Neubauer, 2007 * * This file is part of TaskBlocks * * TaskBlocks is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * TaskBlocks 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package taskblocks.bugzilla; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.CharArrayWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; import taskblocks.utils.Utils; /** * Instances of this class can submit new bugs to Bugzilla. * It uses it's bug_submit cgi script to submit new bug. The output html page is parsed * to recognize the status of the operation. * * @author j.neubauer */ public class BugzillaSubmitter { public static final String BUGID = "bug_id"; /** Bug property name */ public static final String KEYWORDS = "keywords"; /** Bug property name */ public static final String PRODUCT = "product"; /** Bug property name */ public static final String VERSION = "version"; /** Bug property name */ public static final String COMPONENT = "component"; /** Bug property name */ public static final String HARDWARE = "rep_platform"; /** Bug property name */ public static final String OS = "op_sys"; /** Bug property name */ public static final String PRIORITY = "priority"; /** Bug property name */ public static final String SEVERITY = "bug_severity"; /** * Bug property name Probably supported from bugzilla version 3.0, bug only * NEW and ASSIGNED values */ public static final String STATUS = "bug_status"; /** Bug property name */ public static final String ASSIGNED_TO = "assigned_to"; /** Bug property name */ public static final String SUMMARY = "short_desc"; /** Bug property name */ public static final String DESCRIPTION = "comment"; /** Bug property name (aka Original estimation) */ public static final String ESTIMATED_TIME = "estimated_time"; /** Bug property name (aka Hours left) */ public static final String REMAINING_TIME = "remaining_time"; /** Bug property name (aka Current estimation) */ public static final String ACTUAL_TIME = "actual_time"; /** Bug property name */ public static final String BLOCKS = "blocked"; /** Bug property name */ public static final String DEPENDSON = "dependson"; /** Must be enabled on bugzilla server */ public static final String STATUS_WHITEBOARD = "status_whiteboard"; /** * Regular expression used to parse output from bugzilla and to find the submitted bug id. * if not found, it is supposed that error occured. */ public String _successRegexpForSubmit = "Bug ([0-9]+) Submitted"; public String _successRegexpForChange = "Changes submitted for"; /** * Regular expression used to find title of the error if submission doesn't * success. By default, it is the title of the web page */ public String _errTitleRegexp = "<title>(.*)</title>"; /** * Regular expression used to find description of error if submission doesn't * success. This one retrieves the main body of the page. */ public String _errDetailRegexp = "<div id=\"bugzilla-body\">(.*)</div>.*?<div id=\"footer\">"; /** Regular expressions used to clean the detail error message. */ public String[] _errDetailRemovalRegexps = new String[] { "(?s)<script.*?</script>", "(?s)<div id=\"docslinks\">.*?</div>" }; /** * encodes a form data from the given key-value pairs. * * @param formData * @return * @throws UnsupportedEncodingException */ private static String buildFormBody(Map<String, String> formData) throws UnsupportedEncodingException { StringBuilder body = new StringBuilder(); int count = 0; for (Map.Entry<String, String> e : formData.entrySet()) { if (count > 0) { body.append("&"); } body.append(URLEncoder.encode(e.getKey(), "UTF-8")); body.append("="); body.append(URLEncoder.encode(e.getValue(), "UTF-8")); count++; } return body.toString(); } /** * Submits the given body with POST method to specified url * * @param url must be http protocol * @param body * @return http reply data * @throws IOException */ private String submit(URL url, String body) throws IOException { OutputStream out = null; InputStream in = null; HttpURLConnection conn = null; try { // URL must use the http protocol! conn = (HttpURLConnection) url.openConnection(); if(body != null) { conn.setRequestMethod("POST"); conn.setAllowUserInteraction(false); // you may not ask the user conn.setDoOutput(true); // we want to send things // the Content-type should be default, but we set it anyway conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded; charset=utf-8"); // the content-length should not be necessary, but we're cautious conn.setRequestProperty("Content-length", Integer.toString(body.length())); // get the output stream to POST our form data out = conn.getOutputStream(); PrintWriter pw = new PrintWriter(out); pw.print(body); // here we "send" our body! pw.flush(); pw.close(); } else { // ? } // get the input stream for reading the reply // IMPORTANT! Your body will not get transmitted if you get the // InputStream before completely writing out your output first! in = conn.getInputStream(); // Get response. // We hope, that bugzilla results are utf-8 encoded BufferedReader rdr = new BufferedReader(new InputStreamReader(in, "UTF-8")); CharArrayWriter result = new CharArrayWriter(); char[] buf = new char[1024]; int count = rdr.read(buf); while(count > 0) { result.write(buf, 0, count); count = rdr.read(buf); } return result.toString(); } finally { if(out != null) { out.close(); } if(in != null) { in.close(); } if(conn != null) { conn.disconnect(); } } } private void ensureDefault(Map<String, String> map, String key, String defaultValue) { if (!map.containsKey(key)) { map.put(key, defaultValue); } } /** * Submits new bug to bugzilla server running at specified url. * If bugzilla returns error page, and exception is thrown with error message * extracted by parsing the result html page with regular expressions * {@link #_errTitleRegexp}, {@link #_errDetailRegexp} and {@link #_errDetailRemovalRegexps}. * Bug submission success is recognized by parsing output and finding bug id with * regular expressiont {@link #_successRegexpForSubmit}. * * * @param baseUrl * base url of bugzilla server * @param user * user name for authentication * @param password * password for authentication * @param properties * properties of new bug. Use constants in this class as keys. * @return submitted bug id. * * @throws IOException if connection error occures * @throws Exception in other cases. If connection was successfull, error messages are * extracted from the html page. */ public String submit(String baseUrl, String user, String password, Map<String, String> properties) throws Exception { // fill in default values ensureDefault(properties, STATUS, "NEW"); ensureDefault(properties, SEVERITY, "normal"); ensureDefault(properties, PRIORITY, "P2"); ensureDefault(properties, "bug_file_loc", "http://"); // authentication properties.put("form_name", "enter_bug"); properties.put("Bugzilla_login", user); properties.put("Bugzilla_password", password); properties.put("GoAheadAndLogIn", "1"); String formBody = buildFormBody(properties); String result = submit(new URL(baseUrl + "/post_bug.cgi"), formBody); // System.out.println(result); Matcher m = Pattern.compile(_successRegexpForSubmit).matcher(result); if (m.find()) { String bugId = m.group(1); return bugId; } else { String errText = ""; m = Pattern.compile(_errTitleRegexp).matcher(result); if (m.find()) { errText = m.group(1); } String errText2 = ""; m = Pattern.compile(_errDetailRegexp, Pattern.DOTALL).matcher(result); if (m.find()) { errText2 = m.group(1); } if (errText2.length() > 0) { for (String removeRegexp : _errDetailRemovalRegexps) { errText2 = errText2.replaceAll(removeRegexp, ""); } errText2 = errText2.replaceAll("<[^>]*>", ""); errText2 = errText2.replaceAll("\r?\n", " "); errText2 = errText2.replaceAll(" +", " "); } throw new Exception(errText + ": " + errText2); } } public void change(String baseUrl, String user, String password, String bugId, Map<String, String> properties) throws Exception { // fill in bug id properties.put("id", bugId); // authentication //properties.put("form_name", "enter_bug"); properties.put("Bugzilla_login", user); properties.put("Bugzilla_password", password); properties.put("GoAheadAndLogIn", "1"); String formBody = buildFormBody(properties); String result = submit(new URL(baseUrl + "/process_bug.cgi"), formBody); // System.out.println(result); Matcher m = Pattern.compile(_successRegexpForChange).matcher(result); if (m.find()) { return; } else { String errText = ""; m = Pattern.compile(_errTitleRegexp).matcher(result); if (m.find()) { errText = m.group(1); } String errText2 = ""; m = Pattern.compile(_errDetailRegexp, Pattern.DOTALL).matcher(result); if (m.find()) { errText2 = m.group(1); } if (errText2.length() > 0) { for (String removeRegexp : _errDetailRemovalRegexps) { errText2 = errText2.replaceAll(removeRegexp, ""); } errText2 = errText2.replaceAll("<[^>]*>", ""); errText2 = errText2.replaceAll("\r?\n", " "); errText2 = errText2.replaceAll(" +", " "); } throw new Exception(errText + ": " + errText2); } } public Map<String, Map<String, String>> query(String baseUrl, String user, String password, String[] bugs) throws MalformedURLException, IOException, SAXException, ParserConfigurationException { Map<String, String> formData = new HashMap<String, String>(); formData.put("ctype", "xml"); formData.put("Bugzilla_login", user); formData.put("Bugzilla_password", password); formData.put("excludefield", "attachmentdata"); String body = buildFormBody(formData); for(String bugId: bugs) { body += "&"; body += URLEncoder.encode("id", "UTF-8"); body += "="; body += URLEncoder.encode(bugId, "UTF-8"); } String result = submit(new URL(baseUrl + "/show_record.cgi"), body); // parse the resulting xml Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(result.getBytes("UTF-8"))); Element rootE = doc.getDocumentElement(); if(!rootE.getNodeName().equals("bugzilla")) { throw new IOException("Wrong xml answer, doesn't looks like bugzilla"); } Map<String, Map<String, String>> resultData = new HashMap<String, Map<String,String>>(); for(Element bugE: Utils.getChilds(rootE, "bug")) { Map<String, String> bugData = new HashMap<String, String>(); String bugId = fillBugData(bugE, bugData); resultData.put(bugId, bugData); } return resultData; } private String fillBugData(Element bugE, Map<String, String> bugData) { String id = Utils.getFirstElemText(bugE, BUGID); bugData.put(STATUS_WHITEBOARD, Utils.getFirstElemText(bugE, STATUS_WHITEBOARD)); bugData.put(ESTIMATED_TIME, Utils.getFirstElemText(bugE, ESTIMATED_TIME)); bugData.put(ACTUAL_TIME, Utils.getFirstElemText(bugE, ACTUAL_TIME)); bugData.put(REMAINING_TIME, Utils.getFirstElemText(bugE, REMAINING_TIME)); bugData.put(DEPENDSON, Utils.getElemTexts(bugE, DEPENDSON)); bugData.put(BLOCKS, Utils.getElemTexts(bugE, BLOCKS)); return id; } }