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.

2016. március 29., kedd


Lazy init vs initialization by definition for collection typed properties of JPA entities


How to initialize a collection typed property of a JPA entity? In order to make the Unit testing easier and the client code cleaner it is advisable to get an empty collection from the entity. Even if the entity has not been initialized by the JPA (based on database state), or in case of a newly instantiated entity. If null can be returned by the getter, the client code needs to be handle the situation.

In Unit tests, especially by using mocked objects, it can be extremely ugly to let the client code to check for possible null values.

We have two possibilities:

  1. Initialize by declaration
    JPA itself doesn't care whether the collection is initialized or not. When retrieving an Order from the database with JPA, JPA will always return an Order with a non-null list of OrderLines.

    It does so, because a relation can have 0, 1 or N elements, and that is best modeled with an empty, one-sized or N-sized collection. So it's best to make that an invariant by always having a non-null collection, even for newly created instances of the entity. That makes the production code creating new orders safer and cleaner. That also makes your unit tests, using Order instances not coming from the database, safer and cleaner.
    It is also possible to use Guava's immutable collections, e.g.,
    import com.google.common.collect.ImmutableList;
    // ...
    @OneToMany(mappedBy="order")
    List<Order> orders = ImmutableList.of();
    In this way, we are never creating a new empty list, but reusing the empty list of the framework. Therefore the performance argument is no longer relevant.

    In case of the setter can be accessed by the client, it might be a chance, to set the collection to null. In this case, a NullPointerException might occur. Therefore it is a good practice to avoid setting a collection to null in the setter.
  2. Lazy initialization in getter method
    There can be problems with lazy init using with hibernate, related to the search for entities. The JPA specification defines Flushmode to determine if the entities need to be written to the database (see https://en.wikibooks.org/wiki/Java_Persistence/Querying#Flush_Mode for details). In case of lazy init, Hibernate thinks the entity dirty, in case it has been loaded with null collection, but the getter returns an empty collection. If Flush Mode is set to AUTO, Hibernate writes the entity into the database before the search, which might cause performance problems.   

  

2016. március 25., péntek


How to override a function in Mockito. 


So I want not just the return value to be faked, but it should do something completely different as the implementation of the java class, I am mocking. To be concrete, I wanted to add an element into an underlying list in the process() method, instead of let the processing done. At the end of the test method I wanted to check the content of the list, while I wanted to know, how many element has been added in the class under test.

In JMockit it is not a problem at all, while it works exactly like this:


new MockUp<DBManager>() {
   @SuppressWarnings("unused")
   @Mock
   public String retrieveAccountHolderName(int accountId ){
     myList.add(new Integer(accountId)); // myList is declared in the test method
     return "Abhi";
   }
};

So basically you can do whatever you want inside of the mocked method.


To make the problem even harder, I had to inject the mocked object into a service class, while I needed to test this class after all.

Solution:

  • I have created a subclass of AcitveDirectoryDataImporter which overrides the process() method.
  • Defined it as mocked object
  • Let it used by the ActiveDirectoryReaser, using the @InjectMocks annotation
  • Instructed Mockito to use the original process method of MockedAcitveDirectoryDataImporter
  • I also had to create a method, reaching the added elements inside of the MockedAcitveDirectoryDataImporter, and let Mockito to use it's original implementation.

Known limitations:
  1. In case of AcitveDirectoryDataImporter final, the solution will not work. 
  2. The list, containing processed entities had to be declared inside of the new class. It is not possible the class to reach variables of the test class. It is due to the way how Mockito creates the mocked objects. 
Code snippet of the solution:


/**
 * Test class for {@link ActiveDirectoryDataReader}
 * 
 * @author Peter Varga
 *
 */
@RunWith(MockitoJUnitRunner.class)
public class ActiveDirectoryDataReaderTest {

 @Mock
 AdPropertiesHolder propertiesHolder;

 @Mock
 MockedAcitveDirectoryDataImporter acitveDirectoryDataImporter = new MockedAcitveDirectoryDataImporter();

 @Mock
 LdifFileHandlerFactory opcoFileHandlerFactory;

 @InjectMocks
 ActiveDirectoryDataReader activeDirectoryDataReader;

 private void initMockedDependencies(String importDirectoryPath) {
  // Set application properties for test
  when(propertiesHolder.getImportDirectoryPath()).thenReturn(importDirectoryPath);
  when(propertiesHolder.isKeepMarkerFiles()).thenReturn(true);

  // set behavior of mocked objects
  // call original methods of MockedAcitveDirectoryDataImporter
  doCallRealMethod().when(acitveDirectoryDataImporter).process(any(ActiveDirectoryUser.class));
  doCallRealMethod().when(acitveDirectoryDataImporter).getProcessedUsers();
  doCallRealMethod().when(opcoFileHandlerFactory).getInstance(any(String.class));
 }

 @Test
 public void testFindAllWithMultipleFiles() throws Exception {
  String importDirectoryPath = "./src/test/resources/at/a1ta/eap/avatar/dataimport/ad/file/multiple_files";
  initMockedDependencies(importDirectoryPath);

  // call processing method
  activeDirectoryDataReader.findAll();

  List<ActiveDirectoryUser> processedUsers = acitveDirectoryDataImporter.getProcessedUsers();
  Assert.assertEquals(8230, processedUsers.size());
 }

 ... // more test methods here


 /*
  * Class to override default process behavior for AcitveDirectoryDataImporter. It simply adds the ActiveDirectoryUser objects to a list instead of
  * processing them.
  */
 class MockedAcitveDirectoryDataImporter extends AcitveDirectoryDataImporter {
  private List<ActiveDirectoryUser> processedUsers = new ArrayList<>();

  List<ActiveDirectoryUser> getProcessedUsers() {
   if (processedUsers == null) {
    processedUsers = new ArrayList<>();
   }
   return processedUsers;
  }

  @Override
  public void process(ActiveDirectoryUser adUser) {
   getProcessedUsers().add(adUser);
  }
 }