Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • Provide a unique name and optionally description

  • Define properties for the new issue tracking system. Properties are configuration values used by FlexDeploy to connect to the new system. The values of the properties that you define here are set on Issue Tracking Instances, folders, and projects.

    • If you define properties, you can indicate display and validation details. You can also indicate if a property is required and/or encrypted.

    • Enter a unique Name before adding any properties

  • Provide either Java Implementation or Groovy API. If both are supplied, the Java Implementation is used. Java implementation means that you are giving the classpath for a java class which you are placing the jar for in the libext folder. Groovy is defined on the Groovy Api tab. A Groovy script would allow for dynamic update, whereas use of Java code will require restart of server.

  • Click Save.

...

Code Block
languagejava
package mycompany.extension.redmineflexagon.fd.model.integration.its.impl.redmine;

import com.sun.jerseyflexagon.fd.model.integration.its.api.client.ClientIssueTrackingSystem;
import com.sun.jerseyflexagon.fd.model.integration.its.api.client.ClientResponseWorkItem;
import com.sun.jerseyflexagon.fd.model.integration.its.api.client.WebResourceWorkItemAttachment;
import comflexagon.sunfd.jerseymodel.apiintegration.clientits.filterapi.HTTPBasicAuthFilterWorkItemComment;

import javaflexagon.io.Serializable;

import java.util.HashMapfd.model.integration.its.api.WorkItemDetails;
import javaflexagon.util.Map;

import javax.ws.rs.core.Response;

fd.model.integration.its.api.WorkItemStatus;
import flexagon.fd.model.integration.its.api.IssueTrackingSystemWorkItemUpdate;
import flexagon.fd.model.integration.itsutil.api.TicketApiException;
import flexagon.fd.model.integration.util.ApiExceptionRestService;

public class RedmineServiceIntegration
  extends IssueTrackingSystem
{
  public RedmineServiceIntegration()
  {
    super();import flexagon.ff.common.core.exceptions.FlexCheckedException;
import flexagon.ff.common.core.logging.FlexLogger;
import flexagon.ff.common.core.rest.FlexRESTClient;
import flexagon.ff.common.core.rest.FlexRESTClientResponse;
import flexagon.ff.common.core.utils.FlexCommonUtils;

import java.io.InputStream;
import java.io.Serializable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;

import javax.ws.rs.core.MediaType;

public class RedmineIssueTrackingSystem
  extends IssueTrackingSystem
{
  private static final String CLZ_NAM = RedmineIssueTrackingSystem.class.getName();
  private static final FlexLogger LOG = FlexLogger.getLogger(CLZ_NAM);

  private static final String FDREDMINE_URL = "FDREDMINE_URL";
  private static final String FDREDMINE_TICKET_URL_PATTERN = "FDREDMINE_TICKET_URL_PATTERN";
  private static final String FDREDMINE_ISSUE = "{REDMINE_ISSUE}";
  private static final String FDREDMINE_USER_NMAE = "FDREDMINE_USER_NMAE";
  private static final String FDREDMINE_PASSWORD = "FDREDMINE_PASSWORD";

  public RedmineIssueTrackingSystem()
  {
    super();
  }

  private StringBuilder getURLBuilder()
  {
    StringBuilder urlBuilder = new StringBuilder((String) getProperties().get(FDREDMINE_URL));
    return urlBuilder;
  }


  private String getPropertyForTicket(String propertyName, String pTicketNummber)
  {
    String origPropValue = (String) getProperties().get(propertyName);
    if (origPropValue != null)
    {
      return origPropValue.replace(FDREDMINE_ISSUE, pTicketNummber);
    }
    else
    {
      return null;
    }
  }

  private String getURLProperty(String propertyName, String pTicketNummber)
  {
    String methodName = "getURLProperty";
    LOG.logFinestEntering(methodName);

    StringBuilder ticketURLBuilder = getURLBuilder();
    String urlProperty = ticketURLBuilder.append(getPropertyForTicket(propertyName, pTicketNummber)).toString();

    LOG.logFinestExiting(methodName, urlProperty);
    return urlProperty;
  }

  private String getHostNameURL()
  {
    return getURLBuilder().toString();
  }

  private String getUserName()
  {
    return (String) getProperties().get(FDREDMINE_USER_NMAE);
  }

  private String getPassword()
  {
    return (String) getProperties().get(FDREDMINE_PASSWORD);
  }

  private FlexRESTClient getRestService()
    throws ApiException
  {
    return RestService.getService(getHostNameURL(), getUserName(), getPassword());
  }

  private FlexRESTClient getTicketWebResource(String pTicketNummber)
    throws ApiException
  {
    String ticketPath = getPropertyForTicket(FDREDMINE_TICKET_URL_PATTERN, pTicketNummber);
    return getRestService().path(ticketPath);
  }

  public String getWorkItemStatus(WorkItem ticket)
    throws ApiException
  {
    String methodName = "getTicketStatus";
    LOG.logFinestEntering(methodName, ticket.getNumber());

    FlexRESTClientResponse response;
    try
    {
      response = RestService.checkResponse(getTicketWebResource(ticket.getNumber()).get());
    }
    catch (FlexCheckedException e)
    {
      throw new ApiException(e);
    }

    JsonObject jsonObject = RestService.readJsonObject(response.getResponseString());
    JsonObject issueObject = (JsonObject) jsonObject.get("issue");
    String statusName = null;
    if (issueObject != null)
    {
      javax.json.JsonObject status = issueObject.getJsonObject("status");
      if (status != null)
      {
        statusName = status.getString("name");
      }

    }
    LOG.logFinestExiting(methodName, statusName);
    return statusName;
  }

  /*
   * Populates ticket's data (e.g. Description with summary)
   */
  @Override
  public void populateWorkItem(WorkItem ticket)
    throws ApiException
  {
    String methodName = "populateWorkItem";
    LOG.logFinestEntering(methodName, ticket);

    FlexRESTClientResponse response;
    try
    {
      response = RestService.checkResponse(getTicketWebResource(ticket.getNumber()).get());
    }
    catch (FlexCheckedException e)
    {
      throw new ApiException(e);
    }

    JsonObject jsonObject = RestService.readJsonObject(response.getResponseString());
    if (jsonObject != null)
    {
      javax.json.JsonObject issue = jsonObject.getJsonObject("issue");
      if (issue != null)
      {
        ticket.setDescription(issue.getString("description"));
        javax.json.JsonObject tracker = issue.getJsonObject("tracker");
        if (tracker != null)
        {
          ticket.setType(tracker.getString("name"));
        }
      }
    }
    LOG.logFinestExiting(methodName);
  }


  /*
   * Builds absolute ticket's URL
   */
  @Override
  public String getWorkItemURL(WorkItem ticket)
    throws ApiException
  {
    String methodName = "getWorkItemURL";
    LOG.logFinestEntering(methodName, ticket);

    String url = getURLProperty(FDREDMINE_TICKET_URL_PATTERN, ticket.getNumber());
    url = url.replace(".json", "");
    LOG.logFinestExiting(methodName, url);
    return url;
  }

  /*
   * Adds a comment to a ticket
   *
   */
  @Override
  public void addCommentToWorkItem(WorkItem ticket, String comment)
    throws ApiException
  {
    String methodName = "addCommentToWorkItem";
    LOG.logFinestEntering(methodName, ticket.getNumber(), comment);
    JsonObject commentJson = Json.createObjectBuilder().add("issue", Json.createObjectBuilder().add("notes", comment)).build();

    FlexRESTClientResponse response;
    try
    {
      response = RestService.checkResponse(getTicketWebResource(ticket.getNumber()).mediatype(MediaType.APPLICATION_JSON_TYPE).put(commentJson.toString()));
    }
    catch (FlexCheckedException e)
    {
      throw new ApiException(e);
    }

    LOG.logFinestExiting(methodName);
  }

  @SuppressWarnings("oracle.jdeveloper.java.insufficient-catch-block")
  private int getStatusIdFromName(String pStatus)
    throws ApiException
  {
    String methodName = "getStatusIdFromName";
    LOG.logFinestEntering(methodName, pStatus);

    int status_id = -1;
    try
    {
      String statusNumber = pStatus.split("##")[0];
      // If user saves 10 or 10##In Progress, let's try to parse it as number
      status_id = Integer.parseInt(statusNumber);
    }
    catch (Exception nfe)
    {
      // not integer value, call API
    }

    if (status_id == -1)
    {
      // call API to do conversion
      try
      {
        FlexRESTClientResponse response = RestService.checkResponse(getRestService().path("/issue_statuses.json").get());
        Map<String, Integer> statusMap = parseIssueStatuses(response.getResponseString());
        if (statusMap.containsKey(pStatus))
        {
          status_id = statusMap.get(pStatus);
        }
      }
      catch (ApiException apie)
      {
        throw apie;
      }
      catch (FlexCheckedException e)
      {
        throw new ApiException(e);
      }
    }

    LOG.logFinestExiting(methodName, status_id);
    return status_id;
  }

  private Map<String, Integer> parseIssueStatuses(String pJson)
  {
    String methodName = "parseIssueStatuses";
    LOG.logFinestEntering(methodName, pJson);

    // Example output
    //    {
    //        "issue_statuses": [
    //            {
    //                "id": 1,
    //                "name": "New",
    //                "is_closed": false
    //            },
    //            {
    //                "id": 2,
    //                "name": "In Progress",
    //                "is_closed": false
    //            },
    //            {
    //                "id": 3,
    //                "name": "Resolved",
    //                "is_closed": false
    //            },
    //            {
    //                "id": 4,
    //                "name": "Closed",
    //                "is_closed": true
    //            },
    //            {
    //                "id": 5,
    //                "name": "Rejected",
    //                "is_closed": true
    //            },
    //            {
    //                "id": 6,
    //                "name": "Ready",
    //                "is_closed": false
    //            }
    //        ]
    //    }

    Map<String, Integer> map = new HashMap<>();

    JsonObject jsonObject = RestService.readJsonObject(pJson);
    JsonArray ar = jsonObject.getJsonArray("issue_statuses");
    for (int i = 0; i < ar.size(); i++)
    {
      String name = ar.getJsonObject(i).getString("name");
      int id = ar.getJsonObject(i).getInt("id");
      map.put(name, id);
    }

    LOG.logFinestExiting(methodName, map);
    return map;
  }

  /*
   * Changes ticket's status
   *
   */
  @Override
  public void changeWorkItemStatusTo(WorkItem ticket, Serializable status)
    throws ApiException
  {
    String methodName = "changeWorkItemStatusTo";
    LOG.logFinestEntering(methodName, ticket.getNumber(), status);

    // Redmine API needs id and not status name
    JsonObjectBuilder addStatus = Json.createObjectBuilder().add("notes", "Status updated to " + status);
    addStatus = addStatus.add("status_id", getStatusIdFromName((String) status));
    JsonObject statusJson = Json.createObjectBuilder().add("issue", addStatus).build();

    FlexRESTClientResponse response;
    try
    {
      response = RestService.checkResponse(getTicketWebResource(ticket.getNumber()).mediatype(MediaType.APPLICATION_JSON_TYPE).put(statusJson.toString()));
    }
    catch (FlexCheckedException e)
    {
      throw new ApiException(e);
    }

    LOG.logFinestExiting(methodName);
  }

  @Override
  public void checkConnection()
    throws ApiException
  {
    String methodName = "checkConnection";
    LOG.logFineEntering(methodName);

    try
    {
      String aboutUrl = "/users/current.json";
      LOG.logInfo(methodName, "Checking Redmine connection for user {0} with path {1}", getUserName(), getHostNameURL() + aboutUrl);
      FlexRESTClientResponse clientResponse = getRestService().path(aboutUrl).get();
      LOG.logInfo(methodName, "Successfully invoked test connection URL {0}", clientResponse);
      int statusCode = clientResponse.getResponse().getStatusInfo().getStatusCode();
      if (statusCode == 401)
      {
        throw new ApiException("Invalid credentials.", "");
      }
   @Override   public void checkConnection()RestService.checkResponse(clientResponse);
    throws ApiException LOG.logInfo(methodName, "Test {connection response code looks valid, Stringcheck methodNamecontent =of response"checkConnection");
    System.out.println(methodName + " getting version for project to check connection");
    ClientResponse clientResponse = getWebResource("/projects/1/versions.json").get(ClientResponse.class)  RestService.readJsonObject(clientResponse.getResponseString());
      LOG.logInfo(methodName, "Validated that JSON data was received from test connction URL invocation.");
    System.out.println(methodName + String.format(" Successfully invoked test connection URL %s", clientResponse));
}
    catch (ApiException e)
    {
   int statusCode = clientResponseLOG.getStatusInfo().getStatusCode(logInfo(methodName, "ApiException in Test connection", e);
    if (statusCode == 401)throw e;
    {}
    catch  throw new ApiException("Invalid credentials.", "");(Exception e)
    {
}     if (!(clientResponse.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL))
    {LOG.logInfo(methodName, "Failed in Test connection", e);
        throw new ApiException(clientResponse.getEntity(String.class), clientResponse.getStatusInfo().getReasonPhrase())"Connection failed. " + e.getMessage(), "");
    }
    SystemLOG.out.printlnlogFineExiting(methodName + " Test connection response code looks valid, check content of response");
    System.out.println(methodName + " Validated that JSON data was received from test connection URL invocation.");
  }

  @Override
  public Collection<String> parseWorkItemNumberFromChangeLogs(List<String> pChangeLogMessages, List<String> pTicketPatternList)
    throws ApiException
  {
    String methodName = "parseWorkItemNumberFromChangeLogs";
    LOG.logFinestEntering(methodName, pChangeLogMessages, pTicketPatternList);
  }  Collection<String> ticketNumberList private= StringBuildernew getRedmineURLBuilderArrayList<String>();
  {  Collection<String> parsedTicketNumbers  StringBuilder urlBuilder = new StringBuilder((String) getProperties().get("REDMINE_URL"));
= flexagon.fd.model.integration.its.util.ChangeLogParser.getParser().parse(pChangeLogMessages, pTicketPatternList);
    if (FlexCommonUtils.isNotEmpty(parsedTicketNumbers) && FlexCommonUtils.isNotEmpty(pTicketPatternList))
   return urlBuilder;{
  }    privatefor (String parsedTicketWithPattern: getRedmineURL(parsedTicketNumbers)
  {    {
return getRedmineURLBuilder().toString();   }    privatefor (String getUserName(pattern: pTicketPatternList)
  {      {
return (String) getProperties().get("REDMINE_USER_NMAE");   }    private String getPassword()if (parsedTicketWithPattern.startsWith(pattern))
  {      return (String) getProperties().get("REDMINE_PASSWORD"); {
      }    @Override  String publicticketNumber void populateTicket(Ticket pTicket)= parsedTicketWithPattern.substring(pattern.length(), parsedTicketWithPattern.length());
    throws ApiException   {     // TODO Implement this methodticketNumberList.add(ticketNumber);
        }  }
 @Override   public String getTicketURL(Ticket pTicket) }
   throws ApiException  }
{    }
// TODO Implement this method LOG.logFinestExiting(methodName);
    return nullticketNumberList;
  }

  @Override
  public voidCollection<WorkItemComment> addCommentToTicketgetWorkItemComments(TicketWorkItem pTicket, String pString int i, int i2)
    throws ApiException
  {
    // TODO Implement this method
    return Collections.emptySet();
  }

  @Override
  public voidCollection<WorkItemUpdate> changeTicketStatusTogetWorkItemHistory(TicketWorkItem pTicket, String pStringint i, int i2)
    throws ApiException
  {
    // TODO Implement this method
  }    @Override
  public String getTicketStatus(Ticket pTicket)return Collections.emptySet();
  }

throws ApiException @Override
 { public Collection<WorkItemAttachment> getWorkItemAttachments(WorkItem pTicket)
// TODO Implement this method
    return null;
  }

  private Client getRestClient()
  {
    Client restClient = Client.create throws ApiException
  {
    // TODO Implement this method
    return Collections.emptySet();
  }

restClient.addFilter(new HTTPBasicAuthFilter(getUserName(), getPassword())); @Override
  public WorkItemDetails restClient.setReadTimeout(20000);getWorkItem(WorkItem pTicket)
     restClient.setConnectTimeout(10000);throws ApiException
  {
  return restClient; // TODO }Implement this method
 public WebResource getWebResource(String resource)return null;
  }
throws
ApiException  @Override
{  public InputStream getWorkItemAttachmentContent(WorkItem StringpTicket, methodNameSerializable = "getWebResource";pSerializable)
    throws ApiException
if (resource !={
null && !resource.isEmpty() && !resource.startsWith("/"))
    { // TODO Implement this method
    return null;
 resource +=}
"/"
+ resource; @Override
  public }List<WorkItemDetails> getWorkItems(List<WorkItem> pList)
  WebResource webResource = null;throws ApiException
  {
try    // {TODO Implement this method
   webResource =return getRestClient()Collections.resourceemptyList(getRedmineURL()).path(resource);
  }

}  @Override
  catchpublic List<WorkItemStatus> getAvailableWorkItemStatuses(ExceptionWorkItem epWorkItem)
    {throws ApiException
  {
  throw new ApiException(e.getMessage(), e.getMessage());
    }

    return webResource // TODO Implement this method
    return Collections.emptyList();
  }
}

  • In order to compile your java class, you will need FlexDeployAPI.jar on classpath.

  • Implement all the methods described in the table in the API Implementation section.

  • For any failure connecting to the system or if any issues with the data, then you can throw exception. For example throw new ApiException("Invalid credentials.", "");

  • Once you are ready with unit testing, you can prepare Jar file for your credential store java class and other utility classes. This jar file can be placed on server classpath now.

    • Put this jar file in apache-tomcat-flexdeploy/libext folder.

    • If you are using any third party libraries from your Java implementation, then those jar files will also need to be added to same libext folder.

  •  Update this jar file

Groovy Implementation

Here are the high level steps for a Groovy implementation. You can use any IDE to prepare this implementation. 

...