
org.paylogic.fogbugz.FogbugzManager Maven / Gradle / Ivy
package org.paylogic.fogbugz;
import edu.umd.cs.findbugs.annotations.Nullable;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import java.util.logging.Level;
/**
* Manager for FogbugzCase objects. Use this to retrieve, save and create cases.
*/
@Log
public class FogbugzManager {
private String url;
private String token;
@Getter private String featureBranchFieldname;
@Getter private String originalBranchFieldname;
@Getter private String targetBranchFieldname;
@Getter private String approvedRevisionFieldname;
@Getter @Setter private String ciProjectFieldName;
@Getter private int mergekeeperUserId;
@Getter private int gatekeeperUserId;
/**
* Constructor of FogbugzManager.
*/
public FogbugzManager(String url, String token, @Nullable String featureBranchFieldname,
@Nullable String originalBranchFieldname, @Nullable String targetBranchFieldname,
@Nullable String approvedRevisionFieldname, @Nullable String ciProjectFieldName,
int mergekeeperUserId, int gatekeeperUserId) {
this.url = url;
this.token = token;
this.mergekeeperUserId = mergekeeperUserId;
this.gatekeeperUserId = gatekeeperUserId;
// If user does not want custom fields ignore them.
if (featureBranchFieldname != null) {
this.featureBranchFieldname = featureBranchFieldname;
} else {
this.featureBranchFieldname = "";
}
if (originalBranchFieldname != null) {
this.originalBranchFieldname = originalBranchFieldname;
} else {
this.originalBranchFieldname = "";
}
if (targetBranchFieldname != null) {
this.targetBranchFieldname = targetBranchFieldname;
} else {
this.targetBranchFieldname = "";
}
if (approvedRevisionFieldname != null) {
this.approvedRevisionFieldname = approvedRevisionFieldname;
} else {
this.approvedRevisionFieldname = "";
}
if (ciProjectFieldName != null) {
this.ciProjectFieldName = ciProjectFieldName;
} else {
this.ciProjectFieldName = "";
}
}
/**
* Helper method to create basic URL with authentication token in it.
* @return String with basic URL
*/
private String getFogbugzUrl() {
return this.url + "api.asp?token=" + this.token;
}
/**
* Helper method to create API url from Map, with proper encoding.
* @param params Map with parameters to encode.
* @return String which represents API URL.
*/
private String mapToFogbugzUrl(Map params) throws UnsupportedEncodingException {
String output = this.getFogbugzUrl();
for (String key : params.keySet()) {
String value = params.get(key);
if (!value.isEmpty()) {
output += "&" + URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8");
}
}
FogbugzManager.log.info("Generated URL to send to Fogbugz: " + output);
return output;
}
/**
* Fetches the XML from the Fogbugz API and returns a Document object
* with the response XML in it, so we can use that.
*/
private Document getFogbugzDocument(Map parameters) throws IOException, ParserConfigurationException, SAXException {
URL uri = new URL(this.mapToFogbugzUrl(parameters));
HttpURLConnection con = (HttpURLConnection) uri.openConnection();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
return dBuilder.parse(con.getInputStream());
}
/**
* Retrieves a case using the Fogbugz API by caseId.
* @param id the id of the case to fetch.
* @return FogbugzCase if all is well, else null.
*/
public FogbugzCase getCaseById(int id) throws InvalidResponseException, NoSuchCaseException {
List caseList = this.searchForCases(Integer.toString(id));
if (caseList.size() > 1) {
throw new InvalidResponseException("Expected one case, found multiple, aborting.");
}
return caseList.get(0);
}
/**
* Retrieves cases using the Fogbugz API by a query
* @param query fogbugz search query
* @return List of cases
*/
public List searchForCases(String query) throws InvalidResponseException, NoSuchCaseException {
HashMap params = new HashMap(); // Hashmap defaults to
params.put("cmd", "search");
params.put("q", query);
params.put("cols", "ixBug,tags,fOpen,sTitle,sFixFor,ixPersonOpenedBy,ixPersonAssignedTo" + // No trailing comma
this.getCustomFieldsCSV());
Document doc = null;
try {
doc = this.getFogbugzDocument(params);
} catch (Exception e) {
throw new InvalidResponseException(e.getMessage());
}
// Check for case count in cases tag, so we know wheter to parse the response ever further or not.
int caseCount = 0;
try {
Node casesContainer = doc.getElementsByTagName("cases").item(0);
caseCount = Integer.parseInt(casesContainer.getAttributes().getNamedItem("count").getTextContent());
} catch (NumberFormatException e) {
log.log(Level.INFO, "No valid number in case count XML response.", e);
}
if (caseCount < 1) {
throw new NoSuchCaseException("Fogbugz did not return a case for query id " + query);
}
NodeList caseNodes = doc.getElementsByTagName("case");
ArrayList caseList = new ArrayList();
for (int i = 0; i < caseNodes.getLength(); i++) {
caseList.add(this.constructCaseFromXmlNode(caseNodes.item(i)));
}
return caseList;
}
private FogbugzCase constructCaseFromXmlNode(Node caseNode) {
Element doc = (Element) caseNode;
// Collect tags, and put them in list so we can work with them in a nice way.
ArrayList tags = new ArrayList();
NodeList tagNodeList = doc.getElementsByTagName("tag");
if (tagNodeList != null && tagNodeList.getLength() != 0) {
for (int i = 0; i < tagNodeList.getLength(); i++) {
tags.add(tagNodeList.item(i).getTextContent());
}
}
// Construct case object from retrieved data.
return new FogbugzCase(
Integer.parseInt(doc.getElementsByTagName("ixBug").item(0).getTextContent()),
doc.getElementsByTagName("sTitle").item(0).getTextContent(),
Integer.parseInt(doc.getElementsByTagName("ixPersonOpenedBy").item(0).getTextContent()),
Integer.parseInt(doc.getElementsByTagName("ixPersonAssignedTo").item(0).getTextContent()),
tags,
Boolean.valueOf(doc.getElementsByTagName("fOpen").item(0).getTextContent()),
// The following four field are only to be set if the user wants these custom fields.
// Else we put empty string in there, rest of code understands that.
(this.featureBranchFieldname != null && !this.featureBranchFieldname.isEmpty()) ?
doc.getElementsByTagName(this.featureBranchFieldname).item(0).getTextContent() : "",
(this.originalBranchFieldname != null && !this.originalBranchFieldname.isEmpty()) ?
doc.getElementsByTagName(this.originalBranchFieldname).item(0).getTextContent() : "",
(this.targetBranchFieldname != null && !this.targetBranchFieldname.isEmpty()) ?
doc.getElementsByTagName(this.targetBranchFieldname).item(0).getTextContent() : "",
(this.approvedRevisionFieldname != null && !this.approvedRevisionFieldname.isEmpty()) ?
doc.getElementsByTagName(this.approvedRevisionFieldname).item(0).getTextContent() : "",
(this.ciProjectFieldName != null && !this.ciProjectFieldName.isEmpty()) ?
doc.getElementsByTagName(this.ciProjectFieldName).item(0).getTextContent() : "",
doc.getElementsByTagName("sFixFor").item(0).getTextContent()
);
}
/**
* Retrieves all events for a certain case.
* @param id Case id to fetch events from
* @return list of FogbugzEvents
*/
public List getEventsForCase(int id) {
try {
HashMap params = new HashMap(); // Hashmap defaults to
params.put("cmd", "search");
params.put("q", Integer.toString(id));
params.put("cols", "events");
Document doc = this.getFogbugzDocument(params);
List eventList = new ArrayList();
NodeList eventsNodeList = doc.getElementsByTagName("event");
if (eventsNodeList != null && eventsNodeList.getLength() != 0) {
for (int i = 0; i < eventsNodeList.getLength(); i++) {
Element currentNode = (Element) eventsNodeList.item(i);
// Construct event object from retrieved data.
eventList.add(new FogbugzEvent(
Integer.parseInt(currentNode.getElementsByTagName("ixBugEvent").item(0).getTextContent()), // eventid
id, // caseid
currentNode.getElementsByTagName("sVerb").item(0).getTextContent(), // verb
Integer.parseInt(currentNode.getElementsByTagName("ixPerson").item(0).getTextContent()), // person
Integer.parseInt(currentNode.getElementsByTagName("ixPersonAssignedTo").item(0).getTextContent()), // personAssignedTo
DatatypeConverter.parseDateTime(currentNode.getElementsByTagName("dt").item(0).getTextContent()).getTime(), // dateTime
currentNode.getElementsByTagName("evtDescription").item(0).getTextContent(), // evtDescription
currentNode.getElementsByTagName("sPerson").item(0).getTextContent() // sPerson
));
}
}
return eventList;
} catch (Exception e) {
FogbugzManager.log.log(Level.SEVERE, "Exception while fetching case " + Integer.toString(id), e);
}
return null;
}
/**
* Loop through all FogbugzEvent for given case id, and return last (in time) with assignment to gatekeepers.
* @param caseId
* @return Last event with gatekeeper assignment or null.
*/
public FogbugzEvent getLastAssignedToGatekeepersEvent(int caseId) {
List eventList = this.getEventsForCase(caseId);
Collections.sort(eventList);
Collections.reverse(eventList);
for (FogbugzEvent ev : eventList) {
int person = ev.getPerson();
if (ev.getPersonAssignedTo() == this.gatekeeperUserId
&& person != this.gatekeeperUserId
&& person != this.mergekeeperUserId) {
return ev;
}
}
return null;
}
/**
* Loop through all FogbugzEvent for given case id, and return last (in time) with assignment to given user.
* @param caseId
* @param userId
* @return Last event with user assignment or null.
*/
public FogbugzEvent getLastAssignedTo(int caseId, int userId) {
List eventList = this.getEventsForCase(caseId);
Collections.sort(eventList);
Collections.reverse(eventList);
for (FogbugzEvent ev : eventList) {
int person = ev.getPerson();
if (ev.getPersonAssignedTo() == userId
&& person != this.gatekeeperUserId
&& person != this.mergekeeperUserId) {
return ev;
}
}
return null;
}
/**
* Saves a case to fogbugz using its API.
* Supports creating new cases, by setting caseId to 0 on case object.
* @param fbCase The case to save.
* @param comment A message to pass for this edit.
* @return boolean, true if all is well, else false.
*/
public boolean saveCase(FogbugzCase fbCase, String comment) {
try {
HashMap params = new HashMap();
// If id = 0, create new case.
if (fbCase.getId() == 0) {
params.put("cmd", "new");
params.put("sTitle", fbCase.getTitle());
} else {
params.put("cmd", "edit");
params.put("ixBug", Integer.toString(fbCase.getId()));
}
params.put("ixPersonAssignedTo", Integer.toString(fbCase.getAssignedTo()));
params.put("ixPersonOpenedBy", Integer.toString(fbCase.getOpenedBy()));
params.put("sTags", fbCase.tagsToCSV());
if (this.featureBranchFieldname != null && !this.featureBranchFieldname.isEmpty()) {
params.put(this.featureBranchFieldname, fbCase.getFeatureBranch());
}
if (this.originalBranchFieldname != null && !this.originalBranchFieldname.isEmpty()) {
params.put(this.originalBranchFieldname, fbCase.getOriginalBranch());
}
if (this.targetBranchFieldname != null && !this.targetBranchFieldname.isEmpty()) {
params.put(this.targetBranchFieldname, fbCase.getTargetBranch());
}
if (this.approvedRevisionFieldname != null && !this.approvedRevisionFieldname.isEmpty()) {
params.put(this.approvedRevisionFieldname, fbCase.getApprovedRevision());
}
if (this.ciProjectFieldName != null && !this.ciProjectFieldName.isEmpty()) {
params.put(this.ciProjectFieldName, fbCase.getCiProject());
}
params.put("sFixFor", fbCase.getMilestone());
params.put("sEvent", comment);
Document doc = this.getFogbugzDocument(params);
FogbugzManager.log.info("Fogbugz response got when saving case: " + doc.toString());
// If we got this far, all is probably well.
// TODO: parse XML that gets returned to check status furreal.
return true;
} catch (Exception e) {
FogbugzManager.log.log(Level.SEVERE, "Exception while creating/saving case " + Integer.toString(fbCase.getId()), e);
}
return false;
}
/**
* Additional save method that does not propagate a comment.
* @param fbCase The case to save.
* @return boolean, true if all is well, else false.
*/
public boolean saveCase(FogbugzCase fbCase) {
return this.saveCase(fbCase, "");
}
/**
* Assign case to mergekeepers user id. Note: does not save case.
* @param fbCase the case to set assignedTo on.
* @return modified case.
*/
public FogbugzCase assignToMergekeepers(FogbugzCase fbCase) {
fbCase.setAssignedTo(this.mergekeeperUserId);
return fbCase;
}
/**
* Assign the given case to gatekeepers (uses GatekeeperUserID from global settings)
* @param fbCase The case to edit.
* @return modified case.
*/
public FogbugzCase assignToGatekeepers(FogbugzCase fbCase) {
fbCase.setAssignedTo(this.gatekeeperUserId);
return fbCase;
}
public FogbugzUser getFogbugzUser(int ix) {
HashMap params = new HashMap();
params.put("cmd", "viewPerson");
params.put("ixPerson", "" + ix);
Document doc;
try {
doc = this.getFogbugzDocument(params);
} catch (IOException e) {
FogbugzManager.log.log(Level.SEVERE, "Could not get person with index: " + ix);
return null;
} catch (ParserConfigurationException e) {
FogbugzManager.log.log(Level.SEVERE, "Could not get person with index: " + ix);
return null;
} catch (SAXException e) {
FogbugzManager.log.log(Level.SEVERE, "Could not get person with index: " + ix);
return null;
}
String fullName = doc.getElementsByTagName("sFullName").item(0).getTextContent().trim();
return new FogbugzUser(ix, fullName);
}
/**
* Returns a list of custom field names, comma separated. Starts with a comma.
*/
private String getCustomFieldsCSV() {
String toReturn = "";
if (this.featureBranchFieldname != null && !this.featureBranchFieldname.isEmpty()) {
toReturn += "," + this.featureBranchFieldname;
}
if (this.originalBranchFieldname != null && !this.originalBranchFieldname.isEmpty()) {
toReturn += "," + this.originalBranchFieldname;
}
if (this.targetBranchFieldname != null && !this.targetBranchFieldname.isEmpty()) {
toReturn += "," + this.targetBranchFieldname;
}
if (this.approvedRevisionFieldname != null && !this.approvedRevisionFieldname.isEmpty()) {
toReturn += "," + this.approvedRevisionFieldname;
}
if (this.ciProjectFieldName != null && !this.ciProjectFieldName.isEmpty()) {
toReturn += "," + this.ciProjectFieldName;
}
return toReturn;
}
/**
* Retrieves all milestones and returns them in a nice List of FogbugzMilestone objects.
* @return list of FogbugzMilestones
*/
public List getMilestones() {
try {
HashMap params = new HashMap(); // Hashmap defaults to
params.put("cmd", "listFixFors");
Document doc = this.getFogbugzDocument(params);
List milestoneList = new ArrayList();
NodeList milestonesNodeList = doc.getElementsByTagName("fixfor");
if (milestonesNodeList != null && milestonesNodeList.getLength() != 0) {
for (int i = 0; i < milestonesNodeList.getLength(); i++) {
Element currentNode = (Element) milestonesNodeList.item(i);
// Construct event object from retrieved data.
milestoneList.add(new FogbugzMilestone(
Integer.parseInt(currentNode.getElementsByTagName("ixFixFor").item(0).getTextContent()),
currentNode.getElementsByTagName("sFixFor").item(0).getTextContent(),
Boolean.valueOf(currentNode.getElementsByTagName("fDeleted").item(0).getTextContent()),
Boolean.valueOf(currentNode.getElementsByTagName("fReallyDeleted").item(0).getTextContent())
));
}
}
return milestoneList;
} catch (Exception e) {
FogbugzManager.log.log(Level.SEVERE, "Exception while fetching milestones", e);
}
return new ArrayList();
}
/**
* Creates new Milestone in Fogbugz. Please leave id of milestone object empty.
* Only creates global milestones.
* @param milestone to edit/create
*/
public boolean createMilestone(FogbugzMilestone milestone) {
try {
HashMap params = new HashMap();
// If id = 0, create new case.
if (milestone.getId() != 0) {
throw new Exception("Editing existing milestones is not supported, please set the id to 0.");
}
params.put("cmd", "newFixFor");
params.put("ixProject", "-1");
params.put("fAssignable", "1"); // 1 means true somehow...
params.put("sFixFor", milestone.getName());
Document doc = this.getFogbugzDocument(params);
FogbugzManager.log.info("Fogbugz response got when saving milestone: " + doc.toString());
// If we got this far, all is probably well.
// TODO: parse XML that gets returned to check status furreal.
return true;
} catch (Exception e) {
FogbugzManager.log.log(Level.SEVERE, "Exception while creating milestone " + milestone.getName(), e);
}
return false;
}
public boolean createMilestoneIfNotExists(String milestoneName) {
List milestones = this.getMilestones();
for (FogbugzMilestone milestone : milestones) {
if (milestone.getName().equals(milestoneName)) {
// Milestone already exists, no need to create.
FogbugzManager.log.info("Milestone " + milestoneName + " already exists, not creating.");
return false;
}
}
FogbugzManager.log.info("Creating milestone " + milestoneName + ".");
FogbugzMilestone newMilestone = new FogbugzMilestone(0, milestoneName, false, false);
return this.createMilestone(newMilestone);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy