001: /*
002: * Copyright (C) Jakub Neubauer, 2007
003: *
004: * This file is part of TaskBlocks
005: *
006: * TaskBlocks is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU General Public License as published by
008: * the Free Software Foundation; either version 3 of the License, or
009: * (at your option) any later version.
010: *
011: * TaskBlocks is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * along with this program. If not, see <http://www.gnu.org/licenses/>.
018: */
019:
020: package taskblocks.bugzilla;
021:
022: import java.io.BufferedReader;
023: import java.io.ByteArrayInputStream;
024: import java.io.CharArrayWriter;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.InputStreamReader;
028: import java.io.OutputStream;
029: import java.io.PrintWriter;
030: import java.io.UnsupportedEncodingException;
031: import java.net.HttpURLConnection;
032: import java.net.MalformedURLException;
033: import java.net.URL;
034: import java.net.URLEncoder;
035: import java.util.ArrayList;
036: import java.util.HashMap;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.regex.Matcher;
040: import java.util.regex.Pattern;
041:
042: import javax.xml.parsers.DocumentBuilder;
043: import javax.xml.parsers.DocumentBuilderFactory;
044: import javax.xml.parsers.ParserConfigurationException;
045:
046: import org.w3c.dom.Document;
047: import org.w3c.dom.Element;
048: import org.xml.sax.SAXException;
049:
050: import taskblocks.Utils;
051:
052: /**
053: * Instances of this class can submit new bugs to Bugzilla.
054: * It uses it's bug_submit cgi script to submit new bug. The output html page is parsed
055: * to recognize the status of the operation.
056: *
057: * @author j.neubauer
058: */
059: public class BugzillaSubmitter {
060:
061: /** Bug property name */
062: public static final String KEYWORDS = "keywords";
063:
064: /** Bug property name */
065: public static final String PRODUCT = "product";
066:
067: /** Bug property name */
068: public static final String VERSION = "version";
069:
070: /** Bug property name */
071: public static final String COMPONENT = "component";
072:
073: /** Bug property name */
074: public static final String HARDWARE = "rep_platform";
075:
076: /** Bug property name */
077: public static final String OS = "op_sys";
078:
079: /** Bug property name */
080: public static final String PRIORITY = "priority";
081:
082: /** Bug property name */
083: public static final String SEVERITY = "bug_severity";
084:
085: /**
086: * Bug property name Probably supported from bugzilla version 3.0, bug only
087: * NEW and ASSIGNED values
088: */
089: public static final String STATUS = "bug_status";
090:
091: /** Bug property name */
092: public static final String ASSIGNED_TO = "assigned_to";
093:
094: /** Bug property name */
095: public static final String SUMMARY = "short_desc";
096:
097: /** Bug property name */
098: public static final String DESCRIPTION = "comment";
099:
100: /** Bug property name */
101: public static final String ESTIMATED_TIME = "estimated_time";
102:
103: /** Bug property name */
104: public static final String BLOCKS = "blocked";
105:
106: /** Must be enabled on bugzilla server */
107: public static final String STATUS_WHITEBOARD = "status_whiteboard";
108:
109: /**
110: * Regular expression used to parse output from bugzilla and to find the submitted bug id.
111: * if not found, it is supposed that error occured.
112: */
113: public String _successRegexp = "Bug ([0-9]+) Submitted";
114:
115: /**
116: * Regular expression used to find title of the error if submission doesn't
117: * success. By default, it is the title of the web page
118: */
119: public String _errTitleRegexp = "<title>(.*)</title>";
120:
121: /**
122: * Regular expression used to find description of error if submission doesn't
123: * success. This one retrieves the main body of the page.
124: */
125: public String _errDetailRegexp = "<div id=\"bugzilla-body\">(.*)</div>.*?<div id=\"footer\">";
126:
127: /** Regular expressions used to clean the detail error message. */
128: public String[] _errDetailRemovalRegexps = new String[] {
129: "(?s)<script.*?</script>",
130: "(?s)<div id=\"docslinks\">.*?</div>" };
131:
132: /**
133: * encodes a form data from the given key-value pairs.
134: *
135: * @param formData
136: * @return
137: * @throws UnsupportedEncodingException
138: */
139: private static String buildFormBody(Map<String, String> formData)
140: throws UnsupportedEncodingException {
141: StringBuilder body = new StringBuilder();
142: int count = 0;
143: for (Map.Entry<String, String> e : formData.entrySet()) {
144: if (count > 0) {
145: body.append("&");
146: }
147: body.append(URLEncoder.encode(e.getKey(), "UTF-8"));
148: body.append("=");
149: body.append(URLEncoder.encode(e.getValue(), "UTF-8"));
150: count++;
151: }
152: return body.toString();
153: }
154:
155: /**
156: * Submits the given body with POST method to specified url
157: *
158: * @param url must be http protocol
159: * @param body
160: * @return http reply data
161: * @throws IOException
162: */
163: private String submit(URL url, String body) throws IOException {
164:
165: // URL must use the http protocol!
166: HttpURLConnection conn = (HttpURLConnection) url
167: .openConnection();
168: conn.setRequestMethod("POST");
169: conn.setAllowUserInteraction(false); // you may not ask the user
170: conn.setDoOutput(true); // we want to send things
171: // the Content-type should be default, but we set it anyway
172: conn.setRequestProperty("Content-type",
173: "application/x-www-form-urlencoded; charset=utf-8");
174: // the content-length should not be necessary, but we're cautious
175: conn.setRequestProperty("Content-length", Integer.toString(body
176: .length()));
177:
178: // get the output stream to POST our form data
179: OutputStream rawOutStream = conn.getOutputStream();
180: PrintWriter pw = new PrintWriter(rawOutStream);
181:
182: pw.print(body); // here we "send" our body!
183: pw.flush();
184: pw.close();
185:
186: // get the input stream for reading the reply
187: // IMPORTANT! Your body will not get transmitted if you get the
188: // InputStream before completely writing out your output first!
189: InputStream rawInStream = conn.getInputStream();
190:
191: // Get response.
192: // We hope, that bugzilla results are utf-8 encoded
193: BufferedReader rdr = new BufferedReader(new InputStreamReader(
194: rawInStream, "UTF-8"));
195: CharArrayWriter result = new CharArrayWriter();
196: char[] buf = new char[1024];
197: int count = rdr.read(buf);
198: while (count > 0) {
199: result.write(buf, 0, count);
200: count = rdr.read(buf);
201: }
202:
203: conn.disconnect();
204: return result.toString();
205: }
206:
207: private void ensureDefault(Map<String, String> map, String key,
208: String defaultValue) {
209: if (!map.containsKey(key)) {
210: map.put(key, defaultValue);
211: }
212: }
213:
214: /**
215: * Submits new bug to bugzilla server running at specified url.
216: * If bugzilla returns error page, and exception is thrown with error message
217: * extracted by parsing the result html page with regular expressions
218: * {@link #_errTitleRegexp}, {@link #_errDetailRegexp} and {@link #_errDetailRemovalRegexps}.
219: * Bug submission success is recognized by parsing output and finding bug id with
220: * regular expressiont {@link #_successRegexp}.
221: *
222: *
223: * @param baseUrl
224: * base url of bugzilla server
225: * @param user
226: * user name for authentication
227: * @param password
228: * password for authentication
229: * @param properties
230: * properties of new bug. Use constants in this class as keys.
231: * @return submitted bug id.
232: *
233: * @throws IOException if connection error occures
234: * @throws Exception in other cases. If connection was successfull, error messages are
235: * extracted from the html page.
236: */
237: public String submit(String baseUrl, String user, String password,
238: Map<String, String> properties) throws Exception {
239:
240: // fill in default values
241: ensureDefault(properties, STATUS, "NEW");
242: ensureDefault(properties, SEVERITY, "normal");
243: ensureDefault(properties, PRIORITY, "P2");
244: ensureDefault(properties, "bug_file_loc", "http://");
245:
246: // authentication
247: properties.put("form_name", "enter_bug");
248: properties.put("Bugzilla_login", user);
249: properties.put("Bugzilla_password", password);
250: properties.put("GoAheadAndLogIn", "1");
251:
252: String formBody = buildFormBody(properties);
253: String result = submit(new URL(baseUrl + "/post_bug.cgi"),
254: formBody);
255: // System.out.println(result);
256:
257: Matcher m = Pattern.compile(_successRegexp).matcher(result);
258: if (m.find()) {
259: String bugId = m.group(1);
260: return bugId;
261: } else {
262:
263: String errText = "";
264: m = Pattern.compile(_errTitleRegexp).matcher(result);
265: if (m.find()) {
266: errText = m.group(1);
267: }
268:
269: String errText2 = "";
270: m = Pattern.compile(_errDetailRegexp, Pattern.DOTALL)
271: .matcher(result);
272: if (m.find()) {
273: errText2 = m.group(1);
274: }
275: if (errText2.length() > 0) {
276: for (String removeRegexp : _errDetailRemovalRegexps) {
277: errText2 = errText2.replaceAll(removeRegexp, "");
278: }
279: errText2 = errText2.replaceAll("<[^>]*>", "");
280: errText2 = errText2.replaceAll("\r?\n", " ");
281: errText2 = errText2.replaceAll(" +", " ");
282: }
283: throw new Exception(errText + ": " + errText2);
284: }
285: }
286:
287: public String query(String baseUrl, String user, String password,
288: String[] bugs) throws MalformedURLException, IOException,
289: SAXException, ParserConfigurationException {
290: Map<String, String> formData = new HashMap<String, String>();
291: formData.put("ctype", "xml");
292: formData.put("excludefield", "attachmentdata");
293: String body = buildFormBody(formData);
294:
295: for (String bugId : bugs) {
296: body += "&";
297: body += URLEncoder.encode("id", "UTF-8");
298: body += "=";
299: body += URLEncoder.encode(bugId, "UTF-8");
300: }
301:
302: String result = submit(new URL(baseUrl + "/show_record.cgi"),
303: body);
304:
305: // parse the resulting xml
306: Document doc = DocumentBuilderFactory.newInstance()
307: .newDocumentBuilder().parse(
308: new ByteArrayInputStream(result
309: .getBytes("UTF-8")));
310: Element rootE = doc.getDocumentElement();
311: if (!rootE.getNodeName().equals("bugzilla")) {
312: throw new IOException(
313: "Wrong xml answer, doesn't looks like bugzilla");
314: }
315:
316: List<Map<String, String>> bugsData = new ArrayList<Map<String, String>>();
317: for (Element bugE : Utils.getChilds(rootE, "bug")) {
318: Map<String, String> bugData = new HashMap<String, String>();
319: fillBugData(bugE, bugData);
320: }
321: return result;
322: }
323:
324: private void fillBugData(Element bugE, Map<String, String> bugData) {
325: }
326: }
|