All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.paylogic.fogbugz.FogbugzManager Maven / Gradle / Ivy

There is a newer version: 2.2.12
Show newest version
package org.paylogic.fogbugz;

import edu.umd.cs.findbugs.annotations.Nullable;
import lombok.Getter;
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 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, 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 = "";
        }
    }

    /**
     * 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.
        List 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() : "",

                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) {
            if (ev.getPersonAssignedTo() == this.gatekeeperUserId && ev.getPerson() != this.gatekeeperUserId) {
                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());
            }

            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();
        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;
        }
        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