A következő címkéjű bejegyzések mutatása: Atlassian. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Atlassian. Összes bejegyzés megjelenítése

2016. szeptember 28., szerda

Using Application Link in Atlassian plugins



When implementing complex plug-ins for the Atlassian suite, it might be needed to request information from another Atlassian application. You can see examples for it, when JIRA shows Stash commits, of Confluence pages.

As all Atlassian applications has a REST API, it is possible to make JSON calls directly. Using Application Links for communication between Atlassian products however has following advantages

  • It uses central authentication of the Atlassian applications. You can take advantage of the centralized CROWD user management, and don't need to store user and password in some configuration of your plug-in. You also don't need to set authentication data in your code.
  • You use components of the common com.atlassian.applinks.api package, and it is part of the core features of all Atlassian applications. All needed components are included in com.atlassian.applinks maven group, which is automatically included by the application. Therefore you do not need to add any additional maven dependencies to your project.
  • As application liking is supported by all Atlassian products, you can reach all of them with a single programming API from your add-on.
Disadvantage of the solution is, that you are working with pure JSON responses, so in order to map them into the Java world, you need a mapping framework (like Jackson or Gson), and most probably implement your own classes holding the returned information.

If you want to use a single application, with a more object oriented way, you can consider using product specific API.s like JiraRestClient from the com.atlassian.jira.rest.client.api package.  

Configuring Application link

In order to use functionalities of JIRA from my Confluence plug-in, I have created an Application link between the two servers. In local development environment I had to configure the link to use the "Trusted application" authentication type, in order to get it worked.

I made the following steps to create the application link

  1. Start the JIRA and the Confluence server locally, using the atlassian-run command in the root directory of the plug-ins.
  2. Log in as admin to both applications
  3. In JIRA navigate to Administration/Applications/Application links
  4. Create a new application link following the wizard. The process includes to go to the Confluence administration page and set the inverse link as well.
  5. After the application links are visible on both of the servers, set the authentication for incoming and outgoing link to "OAuth (impersonation)" everywhere.
    Doing so, you configures trusted application connection, and you do not need to bother with authentication in the plug-in code.

Implementing usage of Application link

I have implemented an example to show, how to get a list of all JIRA fields in Confluence using the application link to JIRA.

The call via application link uses the REST API URL providing field information: http://javadeveloper:2990/jira/rest/api/2/field

The result from JIRA looks like this:


[
{"id":"issuetype","name":"Issue Type","custom":false,"orderable":true,
 "navigable":true,"searchable":true,"clauseNames":["issuetype","type"],
 "schema":{"type":"issuetype","system":"issuetype"}},
{"id":"components","name":"Component/s","custom":false,"orderable":true,
  "navigable":true,"searchable":true,"clauseNames":["component"],
  "schema":{"type":"array","items":"component","system":"components"}}, ...

At the first step, I create an ApplicationLinkRequest pointing to the given URL. 


final static int APP_LINK_TIME_OUT = 60000;

// Logger instance
private static final Logger log = LoggerFactory.getLogger(JiraServiceCaller.class);

@Autowired
private ApplicationLinkService appLinkService;

/**
 * Creates ApplicationLinkRequest for calling JIRA REST service, based on the restServiceUrl parameter.
 * 
 * @param restServiceUrl
 * @return
 */
protected ApplicationLinkRequest createApplicationLinkRequest(String restServiceUrl) {
 MethodType methodType = Request.MethodType.POST;
 ApplicationLinkRequest aplrq = createApplicationLinkRequest(restServiceUrl, methodType);
 return aplrq;
}

private ApplicationLinkRequest createApplicationLinkRequest(String restServiceUrl, MethodType methodType) {
 ApplicationLink appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType.class);
 if (appLink == null) {
  log.info("Failed to handle REST request. CredentialsRequiredException occured.");
  throw new JiraConnectionException("Unable to get application link of type 'JiraApplication'");
 }

 ApplicationLinkRequestFactory factory = appLink.createAuthenticatedRequestFactory();
  ApplicationLinkRequest aplrq = null;
 try {
  aplrq = factory.createRequest(methodType, appLink.getRpcUrl() + restServiceUrl);
  aplrq.setSoTimeout(APP_LINK_TIME_OUT);

 } catch (CredentialsRequiredException e) {
  log.warn("Error while creating ApplicationLinkRequest", e);
  throw new JiraConnectionException("Unable to connect JIRA via application link. Error message: '" + e.getMessage() + "'");
 }
 return aplrq;
}


Than I execute the configured ApplicationLinkRequest to get the result of the REST API call.


private static final String REST_JIRA_RETRIEVE_FIELDS = "/rest/api/2/field";
 
/**
 * Retrieves a list containing name of all existing JIRA fields
 */
@Override
public List<String> retrieveFieldNames() {
 List<String> result = new ArrayList<String>();

 try {
  ApplicationLinkRequest alr = createApplicationLinkGetRequest(REST_JIRA_RETRIEVE_FIELDS);
  String jiraResponse = alr.execute();
  if (StringUtils.isNotBlank(jiraResponse)) {
   ObjectMapper mapper = new ObjectMapper();
   JsonJiraField[] myObjects = mapper.readValue(jiraResponse, JsonJiraField[].class);
   for (int i = 0; i < myObjects.length; i++) {
    JsonJiraField jsonJiraField = myObjects[i];
    result.add(jsonJiraField.getName());
   }
  }
 } catch (IOException | ResponseException e) {
  log.warn("Failed to handle REST request. IOException occured.", e.getMessage());
 }
 return result;
}


For mapping between the JSON sting and the Java code, I implemented a simple value object, holding the JIRA field info, and used the ObjectMapper from Jackson to fill it up with data.     

Add parameters to the REST call

The ApplicationLinkRequest makes it possible to add parameters to your REST call as key - value pairs, like this:



alr.addRequestParameters("projectKey", projectKey);






2016. szeptember 21., szerda

Creating event listener for JIRA issue


Issue update event listener makes it possible to when the issue is created or modified in JIRA. As the event listener is registered to the lifecycle of the issue, the listener will be notified independently of the actual GUI element, used to create or update the issue.

Also the event listener will be notified after

  • new issue has been created 
  • issue has been modified via the edit issue popup
  • editing the issue directly on the display issue page
  • modifying the issue by sour application
  • modifying the issue via REST API
You need to implement the listener class. You don't need to implement any particular interface, but create following methods:
  • event handler method, marked with the @EventListener annotation.
  • method for registering the listener, marked with the @PostConstruct annotation.
  • method for unregistering the listener, marked with the @PreDestroy annotation.


package at.a1ta.eap.tasktrack.jira.ppm.fieldsync.eventlistener;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.jira.event.issue.IssueEvent;
import com.atlassian.jira.event.type.EventType;
import com.atlassian.jira.issue.Issue;

/**
 * Issue update event listener. Makes it possible to handle event when the issue is created or modified in JIRA.
 * 
 * @author Peter Varga
 *
 */
public class IssueListener {
 // Logger instance
 private static final Logger log = LoggerFactory.getLogger(IssueListener.class);

 @Autowired
 private EventPublisher eventPublisher;

 @PostConstruct
 private void init() {
  eventPublisher.register(this);
  log.debug("at.a1ta.eap.tasktrack.jira.ppm.fieldsync.eventlistener.IssueListener registered to EventPublisher");
 }

 @PreDestroy
 private void preDestroy() {
  eventPublisher.unregister(this);
  log.debug("at.a1ta.eap.tasktrack.jira.ppm.fieldsync.eventlistener.IssueListener unregistered form EventPublisher");
 }

 @EventListener
 public void onIssueEvent(IssueEvent issueEvent) {
  Long eventTypeId = issueEvent.getEventTypeId();
  if (!isEventTypeToBehandled(eventTypeId)) {
   return;
  }

  Issue issue = issueEvent.getIssue();

  log.info("Issue changed {}", issue);

 }

 /**
  * Answers if the event type needs to be handled by this listener. IN this case when issue created or updated. For more event types, refer to the
  * {@link EventType} class.
  */
 private boolean isEventTypeToBehandled(Long eventTypeId) {
  return eventTypeId.equals(EventType.ISSUE_CREATED_ID) || eventTypeId.equals(EventType.ISSUE_UPDATED_ID);
 }

}


To register the event listener, you need to add the following entries into your atlassian-plugin.xml


<component-import key="eventPublisher" interface="com.atlassian.event.api.EventPublisher" />
<component key="eventListener" class="at.a1ta.eap.tasktrack.jira.ppm.fieldsync.eventlistener.IssueListener">
 <description>Class that processes the incoming JIRA issue events.</description>
</component>

Tips and tricks


  • Do not forget to unregister the listener. If you do, the event handling goes several time at the background, without noticing it, an causes performance problems.
  • The event listener will be fired for all issues on the system. Therefore, in order to avoid performance drops, you need to decide as soon as possible, if the event is relevant to you. I suggest filtering the events by it's type.   


2016. március 31., csütörtök

Create or update user in Atlassian Crowd using REST API client


Atlassian Crowd is a central identity manager application for all Atlassian products (like JIRA or Confluence). During user data synchronization of the Avatar database, I needed to update some user information in the connected Crowd server.

In order to use the REST API of Crowd, we need to use the rest client, provided by Atlassian. By adding the following Maven dependency into your project, you can use the client:


 <dependency>
   <version>2.8.3</version>
   <groupId>com.atlassian.crowd</groupId>
   <artifactId>crowd-integration-client-rest</artifactId>
 </dependency>


Using the client from JAVA code:



/**
 * 
 */
package hu.vargasoft.avatar.dataimport.crowd;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.atlassian.crowd.exception.ApplicationPermissionException;
import com.atlassian.crowd.exception.InvalidAuthenticationException;
import com.atlassian.crowd.exception.InvalidUserException;
import com.atlassian.crowd.exception.OperationFailedException;
import com.atlassian.crowd.exception.UserNotFoundException;
import com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory;
import com.atlassian.crowd.model.user.ImmutableUser;
import com.atlassian.crowd.model.user.User;
import com.atlassian.crowd.service.client.CrowdClient;

/**
 * Proxy for the functionalities of Crowd REST API
 * 
 * @author Peter Varga
 *
 */
@Repository("crowdRestApiProxy")
public class CrowdRestApiProxy {

 // Logger instance
 private static final Logger log = LoggerFactory.getLogger(CrowdRestApiProxy.class);

 @Autowired
 CrowdPropertiesHolder crowdPropertiesHolder;

 private CrowdClient crowdClient;

 @PostConstruct
 void init() {
  crowdClient = new RestCrowdClientFactory().newInstance(crowdPropertiesHolder.getUrl(), crowdPropertiesHolder.getApplicationName(),
    crowdPropertiesHolder.getApplicationPassword());
 }

 /**
  * Finds user with given account name
  * 
  * @param accountName
  *            user account in CROWD. Must not be null, or empty.
  * @return {@link User} object from CROWD, or null if user with given account not found
  */
 User findUser(String accountName) {
  log.debug("Finding CROWD user with account: '{}'", accountName);

  User user = null;
  try {
   user = crowdClient.getUser(accountName);
  } catch (UserNotFoundException | OperationFailedException | ApplicationPermissionException | InvalidAuthenticationException e) {
   log.warn("Exception occurred while retrieving user '{}' from CROWD server: {}", accountName, e.getMessage());
  }

  log.debug("Exiting findUser() with result: {}", user);
  return user;
 }

 /**
  * Updates user data in CROWD database
  * 
  * @param account
  *            of the user to be updated
  * @param lastName
  *            new family name
  * @param firstName
  *            new first name
  * @param emailAddress
  *            new email address
  */
 public void updateUser(String account, String firstName, String lastName, String emailAddress) {
  log.debug("Entering updateUserInCrowd(). Parameter: {} {} {} {}", account, lastName, firstName, emailAddress);

  User crowdUser = findUser(account);
  if (crowdUser == null) {
   return;
  }

  ImmutableUser user = new ImmutableUser(crowdUser.getDirectoryId(), crowdUser.getName(), crowdUser.getDisplayName(), emailAddress,
    crowdUser.isActive(), firstName, lastName, crowdUser.getExternalId());

  try {
   crowdClient.updateUser(user);
  } catch (UserNotFoundException | InvalidUserException | OperationFailedException | ApplicationPermissionException
    | InvalidAuthenticationException e) {
   log.warn("Exception occurred while trying to update CROWD user {}:{} ", account, e.getMessage());
  }
 }
}



As the user object, returned ba the client is immutable, a new ImmutableUser instance needs to be created, 

containing the new data. It should be later used as parameter of updateUser().

Starting the Crowd server locally, using the Atlassian SDK.

In a command window, execute the following command: atlas-run-standalone --product crowd

It starts the Crowd server, and makes it possible to log in to the administration GUI. Just open the following URL in your browser: 
http://localhost:4990/crowd/ and use admin/admin as username/password.

It is important to understand, that the authentication of the Crowd client in your Java code requires an application name, and a corresponding application 
password. It does not allow logging in using a normal user, like the default 'admin' user. Therefore, before the client can be used, it is necessary to register 
an application using Applications/Add application menu.

According to the information, found in Link:  
https://developer.atlassian.com/display/CROWDDEV/Using+the+Crowd+REST+APIs

"Crowd offers a set of REST APIs for use by applications connecting to Crowd.

Please note the main difference between Crowd APIs and the APIs of other applications like JIRA and Confluence: In Crowd, an application is the 
client of Crowd, whereas in JIRA/Confluence a user is the client. For example, when authenticating a request to a Crowd REST resource via basic 
authentication, the application name and password is used (and not a username and password). 
Keep this in mind when using the REST APIs."

As I am using a Windows 7 development environment, as a Virtual Machine image, I needed to add the IP address 10.0.2.15 at the "Remote addresses" 
section. Before adding it, I got the following exception while connecting to the Crowd server:

com.atlassian.crowd.exception.ApplicationPermissionException: HTTP Status 403 - Client with address "10.0.2.15" is forbidden from making requests 
to the application, crowd.