Ok

En poursuivant votre navigation sur ce site, vous acceptez l'utilisation de cookies. Ces derniers assurent le bon fonctionnement de nos services. En savoir plus.

Blog de Nicolas DUMINIL Simplex Software - Page 15

  • How to Secure REST APIs (JAX-RS) with OAuth 2.0, OpenID Connect, Keycloak and Dockers

    This blog ticket aims at demonstrating some of the most modern techniques in the world of the REST API, as follows :

    • Using JAX-RS 2.0 to develop REST APIs. The JAX-RS 2.0 implementation used here is RESTeasy 3.0.19 provided by the Wildfly 10.1.0 application server. Wildfly is the community release of the famous JBoss, one of the most known and used Java EE application servers, currently provided by RedHat under the name of JBoss EAP (Enterprise Application Platform). In its 10.1.0 release, Wildfly supports the Java EE 7 specifications level.
    • Using the Keycloak IAM (Identity and Access Management) server in order to secure our REST API. Keycloak is the community release of the RedHat Single Sign-On product. It encompasses lots of technology stacks like OAuth 2.0, OpenId Connect, SAML, Kerberos and much others. Here we’ll be using the last release of Keycloak server which is the 3.4.2.
    • Using Docker containers to deploy the full solution.

    So, let’s start coding.

    The Customer Management REST API

    For the purposes of our demo, we choose a quite classical business case : a customer management service. This service is exposing a REST API which may be called in order to perform CRUD operations with customers, like create, select, update and remove, as well as different multi-criteria find operations. In order to facilitate the speech clarity, a full maven project was provided in GitHub. Please follow this link in order to download and use it by browsing, building and testing it.

    The project is structured on several layers, as follows :

    • The customer-management project. This is a multi-module maven project hosting the master POM.
    • The customer-management-data module. This module is the JPA (Java Persistence API) layer of the project and defines the entities required by the CRUD operations. In order to simplify things, only such entity is defined : Customer.
    • The customer-management-repository module. The project is based on the « repository » design pattern. According to this design pattern, the data persistence and the data retriveral operations are abstracted such that to be performed through a series of straightforward methods, without the need to deal with database concerns like connections, commands, cursors, or readers. Using this pattern can help achieve loose coupling and can keep domain objects persistence ignorant. Here we use the implementation by Apache Deltaspike Data module of the « repository » pattern.
    • The customer-management-facade module. This module implements the « facade » design pattern in order to provide an access data layer to the enterprise data. This layer consists in a stateless session bean which exposes a set of services above the repository layer.
    • The customer-management-rest module. This module is the REST API and defines a set of JAX-RS (RESTeasy) services above the facade layer.
    • The customer-management-ear module. This is a maven module which unique role is to package the whole stuff as an EAR archive. It also uses the wildfly maven plugin in order to deploy and undeploy the built EAR.

    Now, let’s look in a more detailed manner at all these modules.

    The customer-management-data artifact.

    This maven artifact is a JAR archive containing the domain objects. In our case, there is only one domain object, the Customer class. Here is the listing :

    package fr.simplex_software.customer_management.data;
    ...

    @XmlRootElement(name="customer")
    @XmlAccessorType(XmlAccessType.PROPERTY)

    @Entity
    @Table(name="CUSTOMERS")
    public class Customer implements Serializable
    {
      private static final long serialVersionUID = 1L;
      private BigInteger id;
      private String firstName;
      private String lastName;
      private String street;
      private String city;
      private String state;
      private String zip;
      private String country;

      public Customer()
      {
      }

      public Customer(String firstName, String lastName, String street,
        String city, String state, String zip, String country)
      {
        this.firstName = firstName;
        this.lastName = lastName;
        this.street = street;
        this.city = city;
        this.state = state;
        this.zip = zip;
        this.country = country;
      } 

      public Customer (Customer customer)
      {
        this (customer.firstName, customer.lastName, customer.street,
          customer.city, customer.state, customer.zip, customer.country);
      } 

      @Id
      @SequenceGenerator(name = "CUSTOMERS_ID_GENERATOR", sequenceName = "CUSTOMERS_SEQ")
      @GeneratedValue(strategy = GenerationType.SEQUENCE,
        generator = "CUSTOMERS_ID_GENERATOR")
      @Column(name = "CUSTOMER_ID", unique = true, nullable = false, length = 8)
      public BigInteger getId()
      {
        return id;
      }

      public void setId(BigInteger id)
      {
        this.id = id;
      }

      @XmlElement
      @Column(name = "FIRST_NAME", nullable = false, length = 40)
      public String getFirstName()
      {
        return firstName;
      }

      public void setFirstName(String firstName)
      {
        this.firstName = firstName;
      }
      ...

    The class Customer above uses JPA and JAXB annotations in order to define the domain object, together with the persistence and XML/JSON marshalling options. Besides the most notable things here there is the ID generation strategy which assumes the use of Oracle sequences. A generator named CUSTOMERS_ID_GENERATOR is defined and it creates an Oracle sequence named CUSTOMERS_SEQ used further to generate IDs each time a new customer has to be created. While this works fine with Oracle databases, it also works in the case of H2 in-memory databases, used for testing purposes, as in our case.

    In addition to the JPA annotations, defining the database name and columns, the JAXB annotations aim at marshalling/unmarshalling the Java entities from/into XML/JSON payloads. This conversions are required upon invocation of the REST API, as we’ll see later.

    The customer-management-repository artifact.

    This artifact is the repository layer. It consists in the following interface :

    package fr.simplex_software.customer_management.repository;
    ...


    @Repository
    public interface CustomerManagementRepository
      extends EntityRepository<Customer, BigInteger>, EntityManagerDelegate<Customer>
    {
      public List<Customer> findByLastName (String lastName);
      public List<Customer> findByCountry (String country);
      public List<Customer> findByCity (String city);
      public List<Customer> findByZip (String zip);
      public List<Customer> findByState (String state);
      public List<Customer> findByStreet (String street);
      public List<Customer> findByFirstName(String firstName);
    }

    The interface above extends the Apache Deltaspike EntityRepository generic interface passing to it the domain class Customer as well as the ID class which is BigInteger. The idea here is to provide an EntityRepository for Customer domain objects, identified by BigInteger instances. That’s all. We don’t need to implement anything here, as all the required CRUD methods, like save(), refresh(), flush(), find(), remove(), etc. are already provided out-of-the-box. We just need to declare some customized finders with criteria, using the « findBy… » naming convention, and all these methods will be dynamically generated by the Apache Deltaspike Data module, at the deployment time. Awesome !

    The repository design pattern is a very strong one made quite popular by its initial Spring Data implementation.  Our demo being a Java EE 7 one, we don’t have any reason to use Spring here, hence Apache Deltaspike Data module is a very convenient alternative. It fits much better for business cases using Java EE 7 containers, like Wildfly 10, especially when it comes to the CMT (Container Managed Transactions), while Spring, as a non compliant Java EE framework, needs to integrate with an external transaction manager, with all the additional work and risks that this might involve.

    The customer-management-facade artifact.

    This artifact is the « facade » layer. It consists in the following stateless session bean with no interface :

    package fr.simplex_software.customer_management.facade;
    ...

    @Stateless
    public class CustomerManagementFacade
    {
      private static Logger slf4jLogger = 
        LoggerFactory.getLogger(CustomerManagementFacade.class);
      @Inject
      private CustomerManagementRepository repo;
      @Produces
      @PersistenceContext
      private static EntityManager entityManager; 

      public List<Customer> findByFirstName(String firstName)
      {
        return repo.findByFirstName(firstName);
      }

      public List<Customer> findByLastName(String lastName)
      {
        return repo.findByLastName(lastName);
      } 

      public List<Customer> findByCountry(String country)
      {
        return repo.findByCountry(country);
      } 

      public List<Customer> findByCity(String city)
      {
        return repo.findByCity(city);
      } 

      public List<Customer> findByZip(String zip)
      {
        return null;
      } 

      public List<Customer> findByState(String state)
      {
        return repo.findByState(state);
      } 

      public List<Customer> findByStreet(String street)
      {
        return repo.findByStreet(street);
      } 

      public List<Customer> findAll()
      {
        return repo.findAll();
      } 

      public List<Customer> findAll(int customer, int arg1)
      {
        return repo.findAll(customer, arg1);
      } 

      public Customer findBy(BigInteger customerId)
      {
        return repo.findBy(customerId);
      } 

      ………

    As we can see in the listing above, the implementation is really very simple. The class
    CustomerManagementFacade uses the business delegate design pattern in order to expose the our data repository. The CustomerManagerRepository is injected here and used as a business delegate to provide the desired functionality. Everything is highly simplified by using the EJB container CMT, which automatically sets the boundaries of the transactions, such that to discharge the developer of the responsibility to manually handle the commit/rollback mechanics, very complex in a distribuetd environment. Here we are using the implicit attribute for enterprise bean, whic is TransactionAttributeType.REQUIRED. Based on this attribute, if the calling client is running within a transaction and invokes the enterprise bean’s method, the method executes within the client’s transaction. If the client is not associated with a transaction, the container starts a new transaction before running the method. The Java EE platform standard EntityManager is injected as well such that to take advanatge of the CMT. This is as opposed to the use of Spring or other Java SE techniques, which require to integrate with an external JPA implemntation, with all the additional work and risks that this might involve.

    The customer-management-rest artifact

    This artifact is the actual REST layer. Exactly the same way that the customer-management-facade was using the business delegate design pattern to expose its functionality above the repository layer, this layer uses the same business delegate design pattern to expose behaviour above the facade layer. Here is the code :

    package fr.simplex_software.rest;

    @Path("/customers")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public class CustomerManagementResource
    {
      @EJB
      private CustomerManagementFacade facade;
      @POST
      public Response createCustomer(Customer customer)
      {
        Customer newCustomer = facade.saveAndFlushAndRefresh(customer);
        return Response.created(URI.create("/customers/" + newCustomer.getId())).entity(newCustomer).build();
      }

      @GET
      @Path("{id}")
      public Response getCustomer(@PathParam("id") BigInteger id)
      {
        return Response.ok().entity(facade.findBy(id)).build();
      }

      @PUT
      @Path("{id}")
      public Response updateCustomer(@PathParam("id") BigInteger id, Customer customer)
      {
        Customer cust = facade.findBy(id);
        if (cust == null)
          throw new WebApplicationException(Response.Status.NOT_FOUND);
        Customer newCustomer = new Customer(cust);
        customer.setId(cust.getId());
        facade.save(newCustomer);
        return Response.ok().build();
      }

      @DELETE
      @Path("{id}")
      public Response deleteCustomer(@PathParam("id") BigInteger id)
      {
        facade.removeAndFlush(id);
        return Response.ok().build();
      }

      @GET
      @Path("firstName/{firstName}")
      public Response getCustomersByFirstName(@PathParam("firstName") String firstName)
      {
        return Response.ok().entity(facade.findByFirstName(firstName)).build();
      }

      @GET
      public Response getCustomers()
      {
        return Response.ok().entity(facade.findAll()).build();
      }
    }

    The listing above provide a customer CRUD implemented as a REST API. It exposes GET requests to select customers or to find them based on criteria like their first name or their ID, POST requests to create new customers, PUT requests to update existing customers, DELETE requests to remove customers, etc. All this by delegating to the facade layer. Another way to implement things would have been to directly annotate this class with the @Stateless annotation, in which case our REST layer would have been the facade layer in the same time. We preferd to decouple the facade layer from the REST layer, this having also the advantage to expose two different interfaces, dedicated to different kind of clients : HTTP clients invoking the REST layer and RMI/IIOP clients invoking directly the facade layer. Notice that using CDI to directly inject the repository layer would also have been an interesting possibility, giving a third type of interface to our API.

    Now, the interesting part is the security configuration of the REST API layer. It is deployed as a WAR and, hence, this is done in the web.xml file below :

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
      <module-name>customer-management-rest</module-name>
      <security-constraint>
        <web-resource-collection>
          <web-resource-name>customers</web-resource-name>
          <url-pattern>/services/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
          <role-name>customer-manager</role-name>
        </auth-constraint>
      </security-constraint>
      <login-config>
        <auth-method>KEYCLOAK</auth-method>
      </login-config>
      <security-role>
        <role-name>customer-manager</role-name>
      </security-role>
    </web-app>

    What the XML file above is saying is that our REST resources, which path is /services, are protected for all the HTTP requests (GET, POST, PUT, DELETE) such that only the customer-manager role be able to invoke the associated endpoints. This role has to be defined using the KEYCLOAK security provider

    The customer-management-ear artifact

    This artifact aims at building an EAR archive containing all the previous artifact and deploying it on the Wildfly platform. It is also responsible of the creation of the docker containers running the Wildfly and the Keycloak servers, as well as of their configuration. Everything is driven by docker-compose, a very convenient utility to handle and orchestrate docker containers. Here we are using the docker-compose plugin for maven and here is the associated YAML file :

    version: "2"
    services:
      wfy10:
        image: jboss/wildfly:10.1.0.Final
        volumes:
          - ./wfy10/customization:/opt/jboss/wildfly/customization/
        container_name: wfy10
        entrypoint: /opt/jboss/wildfly/bin/standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0
        ports:
          - 8080:8080
          - 9990:9990
        depends_on:
          - "keycloak"

      keycloak:
        image: jboss/keycloak:latest
        volumes:
          - ./keycloak/customization:/opt/jboss/keycloak/customization/
        container_name: keycloak
        entrypoint: /opt/jboss/docker-entrypoint.sh -b 0.0.0.0 -bmanagement 0.0.0.0
        ports:
          - 18080:8080
          - 19990:9990
        environment:
          KEYCLOAK_USER: admin
          KEYCLOAK_PASSWORD: admin

    The YAML file above is using docker-compse release 2. It defines two docker containers :

    • One named keycloak, based on the image named jboss/keycloak:latest from the DockerHub registry. It will run the Keycloak 3.4.2 server by exposing the local TCP ports 8080 and 9990 as global host’s ports 18080 and 19990. A read-only volume will e created for this container and mapped to its /opt/jboss/keycloak/customization It contains some configuration scripts. Also, the user named admin, with password admin, will be created for this server.
    • A second container named wfy10, based on the docker image named jboss/wildfly:10.1.0.Final from the DockerHub registry. This container depends on the first one. This dependency means that the authentication and authorization process, for components deployed on the Wildfly server, are performed by the Keycloak server. It exposes the local ports 8080 and 9990 as being the same global hosts ports.

    Now, running the maven POM with this plugin :

         <plugin>
            <groupId>com.dkanejs.maven.plugins</groupId>
            <artifactId>docker-compose-maven-plugin</artifactId>
            <executions>
              <execution>
                <id>up</id>
                <phase>install</phase>
                <goals>
                  <goal>up</goal>
                </goals>
                <configuration>
                  <composeFile>
                    ${project.basedir}/src/main/docker/docker-compose.yml
                  </composeFile>
                  <detachedMode>true</detachedMode>
                </configuration>
              </execution>
              ……….

    Will check-out the two images from DockerHub and will create the two containers. The execution might take sometime, depending on the network latency.

    mkdir tests
    cd tests
    git clone
    https://github.com/nicolasduminil/customer-management.git

    cd customer-management
    mvn –DskipTests clean install

    We assume here that maven is installed and correctly configured with the required repositories. Also docker and docker-compose should be installed. Once the build process finishes, you can check the result as follows :

    nicolas@BEL20:~/workspace/customer-management$ docker ps
    CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                                              NAMES
    fa25d88b75f3        jboss/wildfly:10.1.0.Final   "/opt/jboss/wildfly/…"   5 seconds ago       Up 4 seconds        0.0.0.0:8080->8080/tcp, 0.0.0.0:9990->9990/tcp     wfy10
    05f1ad1377f9        jboss/keycloak:latest        "/opt/jboss/docker-e…"   6 seconds ago       Up 4 seconds        0.0.0.0:18080->8080/tcp, 0.0.0.0:19990->9990/tcp   keycloak


    Here we can see that we have two running containers, named respectivelly keycloak and wfy10. Now we need to configure the servers ran by these two containers such that we can use them for our purposes. The following script we’ll do :

    docker exec -ti keycloak keycloak/customization/customize.sh > /dev/null
    docker exec -ti wfy10 wildfly/customization/customize.sh > /dev/null
    docker restart wfy10 > /dev/null
    docker exec -ti wfy10 wildfly/customization/deploy.sh > /dev/null

    The first line of this script is customizing the the keycloak server by running the customize.sh script. Here is the code :

    #!/bin/bash
    WILDFLY_HOME=/opt/jboss/keycloak
    KCADM=$WILDFLY_HOME/bin/kcadm.sh
    $KCADM config credentials --server http://localhost:8080/auth --realm master --user admin --password admin
    $KCADM create users -r master -s username=customer-admin -s enabled=true
    $KCADM set-password -r master --username customer-admin --new-password admin
    $KCADM create clients -r master -s clientId=customer-manager-client
      -s bearerOnly="true" -s "redirectUris=["http://localhost:8080/customer-management/*"]" -s enabled=true
    $KCADM create clients -r master -s clientId=curl -s publicClient="true"
      -s directAccessGrantsEnabled="true" -s "redirectUris=["http://localhost"]" -s enabled=true
    $KCADM create roles -r master -s name=customer-manager
    $KCADM add-roles --uusername customer-admin --rolename customer-manager -r master

    The Keycloak server consists in a set of security realms. Each realm is a set of users, roles and clients. In order to secure our REST API we need to create a security realm containing the required users and roles as defined by the web.xml that we showed previously. This might be done in two ways :

    • Using the Keycloak web aministrative console. This is probably the most general and the easiest method but it requires to interact with the console.
    • Using the Keycloak server admin utility, named kcadm. This is a shell script allowing to perform in a programatic way every thing one could do with the admin console. This is the method we are using here as it complies very well with docker and docker-compose.

    First we need to login to the Keycloak server. This is done by the config credentials command. Once logged-in, a JSON file is created and it will be used for the duration of the whole session. Then we create an user named customer-admin. The –r option indicates the name of the security realm. Keycloak comes with a seurity realm by default, called master. We could create a new security realm but we decided to use the existent one, for simplicity sake. This new user needs a password as well and this is created using the set-password command.

    The Keycloak server uses the notion of client, defined as an entity that can request authentication on the behalf of a user. Clients support either the OpenID Connect protocol or SAML. Here we’ll be using OpenID Connect. There are two types of clients :

    • Bearer-only clients. This is a term specific to the OAuth 2.0 protocol. The short explanation is that, in order to access securized services, a client needs an OAuth 2.0 Access Token. This token is created by the OAuth 2.0 implementation. Keycloak is an OAuth 2.0 implementation and, as such, it is able to create and provide to clients an access token such that they be able to invoke the given services. In order to get an access token, a client needs a bearer token, also called refresh token. So a bearer-only client is a Keycloak client that will only verify bearer tokens and can’t obtain the access tokens itself. This means that the caller, which is a service and not an user, is not going to be redirected to a login page, such that it would be the case of an web application. This is exactly what we need in a case where a service needs authentication and authorization in order to call another service.
    • Public clients. This category of clients are able to obtain an access token based on a certain form of authetication.

    In our case, we need two clients : a bearer-only client that will be used for the caller service authentication and authorization purposes, and a public client of the behalf of which we will manually get an access token by providing an username and a password. This is what happens in the script above, the two create clients commands. The first command is creating a bearer-only client, named customer-manager-client. This is the client that will be used by the caller. The second service, named curl, is a public one. It enables the Direct Grant Access (another OAuth 2.0 specific term) meaning that it is able to swap a username and a password for a token.

    The last two lines of the script create the role customer-manager, specified by the security configuration in the web.xml file. Once created, this role is assigned to the user customer-admin on the behalf of the add-roles command.

    The way that the Keycloak documentation recommends to use these two clients, is as follows :
     

    RESULT=`curl --data "grant_type=password&client_id=curl&username=customer-admin&password=admin"
     http://localhost:18080/auth/realms/master/protocol/openid-connect/token`
    TOKEN=`echo $RESULT | sed 's/.*access_token":"//g' | sed 's/".*//g'`

    This example would work on Linux as it relies on the utilization of the curl and sed utilities. Basically what happens here is that the first command, the curl request, invokes the Keycloak OpenID Connect token endpoint with grant type set to password. This triggers another OAuth 2.0 specific thing called Resource Owner Credentials Flow that allows to obtain an access token based on the provided credentials. This request assumes that the Keycloak OpenId Connect endpoint is accessible via the port 18080 of the localhost. Next, the access token is extracted from the curl result by the sed command. Now, this access token may be used by the caller service in order to authenticate against the called service. Now our Keycloak server, ran by the docker container with the same name, is configured and ready to be used. We need to configure the Wildfly server such that to take advantage of the Keycloak authentication/authorization and to deploy to it our EAR. This happens in the next lines of the setup.sh above. First, the wildfly/customization/customize.sh script is called in order to customize the Wildfly server. Here is the code 

    #!/bin/bash
    WILDFLY_HOME=/opt/jboss/wildfly
    JBOSS_CLI=$WILDFLY_HOME/bin/jboss-cli.sh
    $WILDFLY_HOME/bin/add-user.sh admin admin
    if [ ! -f  ./keycloak-wildfly-adapter-dist-3.4.2.Final.tar.gz ]
    then
      curl -O -s https://downloads.jboss.org/keycloak/3.4.2.Final/adapters/keycloak-oidc/keycloak-wildfly-adapter-dist-3.4.2.Final.tar.gz
    fi
    tar xzf keycloak-wildfly-adapter-dist-3.4.2.Final.tar.gz -C $WILDFLY_HOME
    $JBOSS_CLI --file=$WILDFLY_HOME/bin/adapter-install-offline.cli

    In order to configure Wildfly application server for Keycloak authentication, we need to download and install the keycloak adapter for Wildfly. This is what this script is doing. Once downloaded, the archive is stored in the Wildfly home directory and then the adapter-install-offline.cli is called. We only need to restart the Wildfly server such that to take advantage of the new configuration and, after that, on the behalf of the CLI, to deploy our EAR archive.

    Testing our REST API

    Now, once that our two docker containers are running and that our two servers are configured, we are ready for testing. The only last point to check is the file customer-management/customer-management-rest/src/main/webapp/WEB-INF/keycloak.json

    {
      "realm": "master",
      "bearer-only": true,
      "auth-server-url": "http://172.18.0.2:8080/auth",
      "ssl-required": "external",
      "resource": "customer-manager-client",
      "confidential-port": 0,
      "enable-cors": true
    }

    We need to make sure that the IP address in the 3rd line is the public IP address of the docker container running the Keycloak server (the one we called keycloak). This is quite easy to check, for example, by doing the following command :

    docker inspect --format '{{ .NetworkSettings.IPAddress }}' keycloak

    Once that this point is checked, we are ready to star tour integration tests. We are using here the failsafe maven plugin. For example :

    cd tests/customer-manager
    mvn failsafe:integration-test

    This will launch the integration test named  CustomerServiceTestIT which listing is provided here below :

    package fr.simplex_software.rest;

    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public class CustomerServiceTestIT
    {
      private static Logger slf4jLogger = LoggerFactory.getLogger(CustomerServiceTestIT.class);
      private static Client client;
      private static WebTarget webTarget;
      private static Customer customer = null;
      private static String token;

      @BeforeClass
      public static void init() throws Exception
      {
        token = Keycloak.getInstance("http://172.18.0.2:8080/auth", "master", "customer-admin",
          "admin", "curl").tokenManager().getAccessToken().getToken();
      }

      @Before
      public void setUp() throws Exception
      {
        client = ClientBuilder.newClient();
        webTarget = client.target("http://localhost:8080/customer-management/services/customers");
      }

      @After
      public void tearDown() throws Exception
      {
        if (client != null)
        {
          client.close();
          client = null;
        }
        webTarget = null;
      }

      @AfterClass
      public static void destroy()
      {
        token = null;
      }

      @Test
      public void test1() throws Exception
      {
        slf4jLogger.debug("*** Create a new Customer ***");
        Customer newCustomer = new Customer("Nick", "DUMINIL", "26 Allée des Sapins", "Soisy sous Montmorency",
          "None", "95230", "France");
        Response response = webTarget.request().header(HttpHeaders.AUTHORIZATION, "Bearer " +
          token).post(Entity.entity(newCustomer, "application/json"));
        assertEquals(201, response.getStatus());
        customer = response.readEntity(Customer.class);
        assertNotNull(customer);
        String location = response.getLocation().toString();
        slf4jLogger.debug("*** Location: " + location + " ***");
        response.close();
      }

      @Test
      public void test2()
      {
        String customerId = customer.getId().toString();
        slf4jLogger.debug("*** Get a Customer with ID {} ***", customerId);
        slf4jLogger.info("*** token: {}", token);
        Response response = webTarget.path(customerId).request().header(HttpHeaders.AUTHORIZATION,
          "Bearer " + token).get();
        assertEquals(200, response.getStatus());
        customer = response.readEntity(Customer.class);
        assertNotNull(customer);
        assertEquals(customer.getCountry(), "France");
      }

      @Test
      public void test3()
      {
        String firstName = customer.getFirstName();
        slf4jLogger.debug("*** Get a Customer by first name {} ***", firstName);
        Response response =
          ebTarget.path("firstName").path(firstName).request().header(HttpHeaders.AUTHORIZATION,
          "Bearer " + token).get();
        assertEquals(200, response.getStatus());
        List<Customer> customers = response.readEntity(new GenericType<List<Customer>>(){});
        assertNotNull(customers);
        assertTrue(customers.size() > 0);
        customer = customers.get(0);
        assertNotNull(customer);
        assertEquals(customer.getCountry(), "France");
      }

      @Test
      public void test4()
      {
        String customerId = customer.getId().toString();
        slf4jLogger.debug("*** Update the customer with ID {} ***", customerId);
        customer.setCountry("Belgium");
        Response response = webTarget.path(customerId).request().header(HttpHeaders.AUTHORIZATION,
          "Bearer " + token).put(Entity.entity(customer, "application/json"));
        assertEquals(200, response.getStatus());
      }

      @Test
      public void test5()
      {
        String customerId = customer.getId().toString();
        slf4jLogger.debug("*** Delete the customer with ID {} ***", customerId);
        Response response = webTarget.path(customerId).request().header(HttpHeaders.AUTHORIZATION,
          "Bearer " + token).delete();
        assertEquals(200, response.getStatus());
      }

      @Test
      public void test6()
      {
        Response response = webTarget.request().header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
        assertEquals(200, response.getStatus());
        List<Customer> customers = response.readEntity(new GenericType<List<Customer>>(){});
        assertNotNull(customers);
        assertTrue(customers.size() > 0);
        customer = customers.get(0);
        assertNotNull(customer);
        assertEquals(customer.getCountry(), "France");
      }
    }

    As we can see, this is a simple test calling all the endpoint of our REST API. The only more notable thing is the way that we are getting the OAuth 2.0 access token. This happens in the init() method. It is annotated with the @BeforeClass annotation, meaning that it is executed only once, in the beggining, before the individual tests are done. The Java code we have here is the exact equivalent of the curl/sed commands combination presented above. The getInstance() methos gets an instance of the Keycloak server by providing the login information, as follows :

    • The Keycloak OpenID Connect endpoint which is http://<ip-address>:<tcp-port>/auth, where <ip-address> is the keycloak docker container public IP addres (172.18.0.2 in our case), and <tcp-port> is the internal docker container tcp port where the Keycloak server is listening for requests (8080 in our case). These informtion should match the ones in the keycloak.json file, as mentioned previously. Please notice that, while the Keycloak server is accessible from the host running the docker container at the address localhost :18080, from inside the container, this address is 172.18.0.2 :8080.
    • The name of the realm which, in our case, is master. This information should also match the one in keycloak .json.
    • The credentials : username (customer-admin) and password (admin).
    • The name of the Keycloak client having enabled the Direct Grant Acess of the OAuth 2.0 protocol. In our case we provided a client named curl. Please notice that there is no any realtion with the utility having the same name.

    Once logged-in to the Keycloak server, we can get the TokenManager and, on its behalf, the AccessToken and, then the string representation of the access token itself. This string representation of the access token is stored in a static global property and injected as an AUTHORIZATION header with each HTTP request.

    The listing below shows a fragment of the log file created by the test execution. You can see inside the Authorization: Bearer header. We replaced the token content by « … » in order to simplify things. At the end, you should see Tests run : 6, Failures : 0, Skipped : 0.

    -------------------------------------------------------
     T E S T S
    -------------------------------------------------------
    Running fr.simplex_software.rest.CustomerServiceIT
    0    [main] DEBUG org.apache.http.impl.conn.PoolingClientConnectionManager  - Connection request: [route: {}->http://172.18.0.2:8080][total kept alive: 0; route allocated: 0 of 10; total allocated: 0 of 10]
    7    [main] DEBUG org.apache.http.impl.conn.PoolingClientConnectionManager  - Connection leased: [id: 0][route: {}->http://172.18.0.2:8080][total kept alive: 0; route allocated: 1 of 10; total allocated: 1 of 10]
    9    [main] DEBUG org.apache.http.impl.conn.DefaultClientConnectionOperator  - Connecting to 172.18.0.2:8080
    20   [main] DEBUG org.apache.http.client.protocol.RequestAddCookies  - CookieSpec selected: best-match
    24   [main] DEBUG org.apache.http.client.protocol.RequestAuthCache  - Auth cache not set in the context
    24   [main] DEBUG org.apache.http.client.protocol.RequestTargetAuthentication  - Target auth state: UNCHALLENGED
    24   [main] DEBUG org.apache.http.client.protocol.RequestProxyAuthentication  - Proxy auth state: UNCHALLENGED
    24   [main] DEBUG org.apache.http.impl.client.DefaultHttpClient  - Attempt 1 to execute request
    24   [main] DEBUG org.apache.http.impl.conn.DefaultClientConnection  - Sending request: POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
    25   [main] DEBUG org.apache.http.wire  -  >> "POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Accept: application/json[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Accept-Encoding: gzip, deflate[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Content-Type: application/x-www-form-urlencoded[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Content-Length: 73[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Host: 172.18.0.2:8080[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "Connection: Keep-Alive[r][n]"
    26   [main] DEBUG org.apache.http.wire  -  >> "[r][n]"
    26   [main] DEBUG org.apache.http.headers  - >> POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
    26   [main] DEBUG org.apache.http.headers  - >> Accept: application/json
    26   [main] DEBUG org.apache.http.headers  - >> Accept-Encoding: gzip, deflate
    26   [main] DEBUG org.apache.http.headers  - >> Content-Type: application/x-www-form-urlencoded
    26   [main] DEBUG org.apache.http.headers  - >> Content-Length: 73
    26   [main] DEBUG org.apache.http.headers  - >> Host: 172.18.0.2:8080
    26   [main] DEBUG org.apache.http.headers  - >> Connection: Keep-Alive
    27   [main] DEBUG org.apache.http.wire  -  >> "grant_type=password&username=customer-admin&password=admin&client_id=curl"
    131  [main] DEBUG org.apache.http.wire  -  << "HTTP/1.1 200 OK[r][n]"
    132  [main] DEBUG org.apache.http.wire  -  << "Connection: keep-alive[r][n]"
    132  [main] DEBUG org.apache.http.wire  -  << "Set-Cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/master; HttpOnly[r][n]"
    132  [main] DEBUG org.apache.http.wire  -  << "Content-Type: application/json[r][n]"
    133  [main] DEBUG org.apache.http.wire  -  << "Content-Length: 2423[r][n]"
    133  [main] DEBUG org.apache.http.wire  -  << "Date: Thu, 04 Jan 2018 11:06:44 GMT[r][n]"
    133  [main] DEBUG org.apache.http.wire  -  << "[r][n]"
    133  [main] DEBUG org.apache.http.impl.conn.DefaultClientConnection  - Receiving response: HTTP/1.1 200 OK
    133  [main] DEBUG org.apache.http.headers  - << HTTP/1.1 200 OK
    133  [main] DEBUG org.apache.http.headers  - << Connection: keep-alive
    134  [main] DEBUG org.apache.http.headers  - << Set-Cookie: KC_RESTART=; Version=1; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Max-Age=0; Path=/auth/realms/master; HttpOnly
    134  [main] DEBUG org.apache.http.headers  - << Content-Type: application/json
    134  [main] DEBUG org.apache.http.headers  - << Content-Length: 2423
    134  [main] DEBUG org.apache.http.headers  - << Date: Thu, 04 Jan 2018 11:06:44 GMT
    153  [main] DEBUG org.apache.http.client.protocol.ResponseProcessCookies  - Cookie accepted [KC_RESTART="", version:1, domain:172.18.0.2, path:/auth/realms/master, expiry:Thu Jan 01 01:00:10 CET 1970]
    154  [main] DEBUG org.apache.http.impl.client.DefaultHttpClient  - Connection can be kept alive indefinitely
    264  [main] DEBUG org.apache.http.wire  -  << "{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1WW1JaW50R09uZ2J0RExCYzREeUJKQVlpRk5LdUNTS1VJRVYyaUNFekZZIn0.eyJqdGkiOiI2NGJiZTAyZS00Zjk4LTRlMGEtOGFiNy1iNzVkNTQzZTFmN2QiLCJleHAiOjE1MTUwNjQwNjQsIm5iZiI6MCwiaWF0IjoxNTE1MDY0MDA0LCJpc3MiOiJodHRwOi8vMTcyLjE4LjAuMjo4MDgwL2F1dGgvcmVhbG…..

    308  [main] DEBUG org.apache.http.impl.conn.PoolingClientConnectionManager  - Connection [id: 0][route: {}->http://172.18.0.2:8080] can be kept alive indefinitely
    308  [main] DEBUG org.apache.http.impl.conn.PoolingClientConnectionManager  - Connection released: [id: 0][route: {}->http://172.18.0.2:8080][total kept alive: 1; route allocated: 1 of 10; total allocated: 1 of 10]
    316  [main] DEBUG fr.simplex_software.rest.CustomerServiceIT  - *** Create a new Customer ***
    335  [main] DEBUG org.apache.http.impl.conn.BasicClientConnectionManager  - Get connection for route {}->http://localhost:8080
    335  [main] DEBUG org.apache.http.impl.conn.DefaultClientConnectionOperator  - Connecting to localhost:8080
    336  [main] DEBUG org.apache.http.client.protocol.RequestAddCookies  - CookieSpec selected: best-match
    336  [main] DEBUG org.apache.http.client.protocol.RequestAuthCache  - Auth cache not set in the context
    336  [main] DEBUG org.apache.http.client.protocol.RequestProxyAuthentication  - Proxy auth state: UNCHALLENGED
    336  [main] DEBUG org.apache.http.impl.client.DefaultHttpClient  - Attempt 1 to execute request
    336  [main] DEBUG org.apache.http.impl.conn.DefaultClientConnection  - Sending request: POST /customer-management/services/customers HTTP/1.1
    336  [main] DEBUG org.apache.http.wire  -  >> "POST /customer-management/services/customers HTTP/1.1[r][n]"
    336  [main] DEBUG org.apache.http.wire  -  >> "Accept-Encoding: gzip, deflate[r][n]"
    337  [main] DEBUG org.apache.http.wire  -  >> "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1WW1JaW50R09uZ2J0RExCYzREeUJKQVlpRk5LdUNTS1VJRVYyaUNFekZZIn0.eyJqdGkiOiI2NGJiZTAyZS00Zjk4LTRlMGEtOGFiNy1iNzVkNTQzZTFmN2QiLCJleHAiOjE1MTUwNjQwNjQsIm5iZiI6MCwiaWF0IjoxNTE1MDY0MDA0LCJpc3MiOiJodHRwOi8vMTcyLjE4LjAuMjo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1…

    337  [main] DEBUG org.apache.http.wire  -  >> "Content-Type: application/json[r][n]"
    337  [main] DEBUG org.apache.http.wire  -  >> "Content-Length: 163[r][n]"
    337  [main] DEBUG org.apache.http.wire  -  >> "Host: localhost:8080[r][n]"
    337  [main] DEBUG org.apache.http.wire  -  >> "Connection: Keep-Alive[r][n]"
    337  [main] DEBUG org.apache.http.wire  -  >> "[r][n]"
    337  [main] DEBUG org.apache.http.headers  - >> POST /customer-management/services/customers HTTP/1.1
    337  [main] DEBUG org.apache.http.headers  - >> Accept-Encoding: gzip, deflate
    337  [main] DEBUG org.apache.http.headers  - >> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1WW1JaW50R09uZ2J0RExCYzREeUJKQVlpRk5LdUNTS1VJRVYyaUNFekZZIn0.eyJqdGkiOiI2NGJiZTAyZS00Zjk4LTRlMGEtOGFiNy1iNzVkNTQzZTFmN2QiLCJleHAiOjE1MTUwNjQwNjQsIm5iZiI6MCwiaWF0IjoxNTE1MDY0MDA0LCJpc3MiOiJodHRwOi8vMTcyLjE4LjAuMjo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1…

    337  [main] DEBUG org.apache.http.headers  - >> Content-Type: application/json
    337  [main] DEBUG org.apache.http.headers  - >> Content-Length: 163
    337  [main] DEBUG org.apache.http.headers  - >> Host: localhost:8080
    337  [main] DEBUG org.apache.http.headers  - >> Connection: Keep-Alive
    337  [main] DEBUG org.apache.http.wire  -  >> "{"id":null,"firstName":"Nick","lastName":"DUMINIL","street":"26 All[0xc3][0xa9]e des Sapins","city":"Soisy sous Montmorency","state":"None","zip":"95230","country":"France"}"
    674  [main] DEBUG org.apache.http.wire  -  << "HTTP/1.1 201 Created[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Expires: 0[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Cache-Control: no-cache, no-store, must-revalidate[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "X-Powered-By: Undertow/1[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Server: WildFly/10[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Pragma: no-cache[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Location: http://localhost:8080/customer-management/services/customers/1[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Date: Thu, 04 Jan 2018 11:06:45 GMT[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Connection: keep-alive[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Content-Type: application/json[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "Content-Length: 160[r][n]"
    676  [main] DEBUG org.apache.http.wire  -  << "[r][n]"
    676  [main] DEBUG org.apache.http.impl.conn.DefaultClientConnection  - Receiving response: HTTP/1.1 201 Created
    676  [main] DEBUG org.apache.http.headers  - << HTTP/1.1 201 Created
    676  [main] DEBUG org.apache.http.headers  - << Expires: 0
    676  [main] DEBUG org.apache.http.headers  - << Cache-Control: no-cache, no-store, must-revalidate
    676  [main] DEBUG org.apache.http.headers  - << X-Powered-By: Undertow/1
    676  [main] DEBUG org.apache.http.headers  - << Server: WildFly/10
    676  [main] DEBUG org.apache.http.headers  - << Pragma: no-cache
    676  [main] DEBUG org.apache.http.headers  - << Location: http://localhost:8080/customer-management/services/customers/1
    676  [main] DEBUG org.apache.http.headers  - << Date: Thu, 04 Jan 2018 11:06:45 GMT
    676  [main] DEBUG org.apache.http.headers  - << Connection: keep-alive
    677  [main] DEBUG org.apache.http.headers  - << Content-Type: application/json
    677  [main] DEBUG org.apache.http.headers  - << Content-Length: 160
    679  [main] DEBUG org.apache.http.impl.client.DefaultHttpClient  - Connection can be kept alive indefinitely
    679  [main] DEBUG org.apache.http.wire  -  << "{"id":1,"firstName":"Nick","lastName":"DUMINIL","street":"26 All[0xc3][0xa9]e des Sapins","city":"Soisy sous Montmorency","state":"None","zip":"95230","country":"France"}"
    684  [main] DEBUG org.apache.http.impl.conn.BasicClientConnectionManager  - Releasing connection org.apache.http.impl.conn.ManagedClientConnectionImpl@3e27aa33
    684  [main] DEBUG org.apache.http.impl.conn.BasicClientConnectionManager  - Connection can be kept alive indefinitely
    684  [main] DEBUG fr.simplex_software.rest.CustomerServiceIT  - *** Location: http://localhost:8080/customer-management/services/customers/1 ***

    Congratulations, you just have configured and run two docker containers, with Keycloak and Wildfly servers, and you tested the REST API authentication/authorization with Oauth 2.0 and OpenId Connect protocols.

  • The Chicken and Pig Story or an Agile Anti Manifesto

    The story has been written by Ken Schwaber, one of the contributors to the Scrum methodology. 

    A pig and a chicken are walking down a road. The chicken looks at the pig and says, “Hey, why don’t we open a restaurant?” The pig looks back at the chicken and says, “Good idea, what do you want to call it?” The chicken thinks about it and says, “Why don’t we call it ‘Ham and Eggs?’” “I don’t think so,” says the pig, “I’d be committed, but you’d only be involved.”

    This fable has been used in order to describe the two types of project members of an agile team:

    • pigs, who are totally committed to the project and accountable for its outcome. For a Scrum project, for example, the development team, the Product Owner and the Scrum Master are pigs;
    • chickens, who aren't commited but only involved and, as such, not accountable for any specific deliverable. Keeping the same example of the Scrum team, the chickens are the different stacke-holders and managers that might be involved, i.e.  consulted on the project and informed of its progress, but not commiteed as not accountable for any specific work.

    As of 2011, the fable has been removed from the official Scrum process. And with good reasons as it didn't give a very positive and enviable image to it. But while the Scrum apologists don't like to much the chicken and pig mataphore, at such a point that they removed it from the Scrum folklore, the story shows however a reality that any Scrum, or other agile practitioner, should be aware of.

    If you're like me, then you've certainly noticed that, at your daily stand-up, there are two categories of participants: the ones talking about theirself and the ones talking about the others. The first category are pigs. They talk about what they did, what they didn't do, why they didn't do what they were suposed to do, what kind of issues preventing them to do their job they meat, what they we'll be doing tomorrow, etc. They are commited. The second category are chickens. They are talking about what meetings they have attended, what the other guys in the meetings said, what did they hear about different events that might affect different enterprise aspects, what kind of decision has been taken in this or that department, etc. They aren't commited as they don't have any precise goal, any defined result to attend, they are just involved, meaning that they are talking about around, they are asking questions, most often irrelevant and time consuming ones, etc. They need to be informed by the others, the pigs, because by themself they aren't able to find the right information.

    This sketch of an agile team gives a very unequal and undemocratic view of how things happen in the nowadays digital enterprise. This goes back to the Agile Manifesto very ideological stance, where personal opinions are turned into a dogma, as this is often the case with "manifesti". But more then a dogma, the Agile Manifesto is an utopia.

    The most utopian point of the Agile is probably its allegedly leaderlessness.  Did you ever see an orchestra which is not guided by a conductor ? I'm not talking, of course, about a band formed of 4 - 5 guys, but about a real orchestra. However, as surprizing as it might be, there is no such a thing like "project manager" in Agile. This means that the team leads itself ! Such a team is supposed to work as a "band of brothers and sisters" and to put mission first. And in order to provide self-organization, the team members need to be colocated, even at the cloud era and, sometimes, to share the same keyboard, mouse and screen. The colocation is also supposed to guarantee that the team members are fully dedicated and permanently available.

    This so-called self-organization is, in fact, a transfer of a significant quantity of work from the chicken area to the pigs one. I a traditional project, the organization is the work of the project managers, i.e. a full time work. This role is considered, for some reasons, as being a non directly productive one, nevertheless is requires a high commitment and, often, much more than 40 hours weekly. Project managerst throw themselves on meeting grenades so that the team can actually get work done. The number one goal of any competent Project Manager is to shield their team from as much of this organizational overhead as possible. So, how is than the self-organization supposed to work ? How is the agile team supposed to compensate the fact that there is no longer anyone to the Project Manager's job ?

    If you're like me, then you certainly have heard many times stacke-holders and line managers saying to developers (the pigs), who were chanlanging them in the organization field, something like: "You need to organize yourself. We're doing agile here !". Agile became the ideal alibi for the managers and stacke-holders to flich from their obligations and responsibilities. Previously, in the context of traditional projects, they were responsible to organize things but now, with agile and the so-called self-organization, the became free like birds to roam around the institution, from meeting to meeting and discover adventure, without any pressure of milestones and deliverables. The result is that managers don't manage any more, while developers aren't given the required resources to coordinate themselves. Consequently, when delivery dates become pressing, i.e. most of times, instead of coordinating itself, the team is disorganized.

    But things are even worst when it comes to documentation. Not only agile became the prefered management alibi to avoid doing its fundamental work but, additionally, if you are like me, you certainly have heard many times that: "We don't have documentation 'cause we're doing agile". This also goes back to the Agile Manifest which says: "Working software rather then documentation".

    This sentence of the Agile Manifesto was probably the most criticized one. What "working software" might mean if there is no documentation explaining how everything is supposed to work ? How could a software tester decide whether a piece of software works if there is no any test plan ? 'Cause test plan is documentation.

    Here again, the agile apologists have explained that the expression "rather than documentation", didn't mean no documentation at all, but spending less time in writing documentation, not trying to define everything from the beggining, doing it in several iterations as opposed to a water-fall process. Well, our friends, the chickens, didn't understand things this way and, since pigs were already renowed for not being very good at writing documentation, the result is that most agile projects have only the code as the unique source of information.

    But the most important impact that agile methods have had on software development projects concern their architecture and design. If we take Scrum as an agile method example, then the work is done in sprints. The sprints content is decided based on the back-log. But the back-log is drafted in terms whic make sense to the Product Owner and to users. These terms come from epics and user's stories and they express users concerns. They are expressed based on templates like: "as a user, I would like to have a function able to ...".  Consequently, the back-log and, with it, the sprints, are drafted in functional and business terms. So, what about software architecture and design ?

    If you are like me, you know that the success of a software system depends on the reliability and robustness of some basics components, often called framework. Things like distributed processing, remote access, security, transactions, race conditions, data persistence, high availiability, etc., are the role of this basic components called framework. What is the place of the framework in the back-log ? Well, probably none.

    Scrum finished by recognizing the importance of the framework and it added the so-called "sprint 0" which is supposed to deal with this components. But the "sprint 0", as viewed by Scrum, is an initial phase of a couple of weeks in which software architects and designers are supposed to agree on a framework. This framework couldn't then change during the whole project life-cycle, because this would block the sprints. This is a very simplified and even naïve view of things to consider that topics of that importance could be decided, one for ever, in the "sprint 0". The Scrum method has evolved since its begginings and, nowadays, it recognize the importance of the so-called "agile architect" supposed to be in charge of the software architecture and design across sprints and to guarantee the change management. This role is somewhere between the pigs and the chickens and most of the Scrum teams don't provide a formal definition of such a role. Hence, no one in the team has a long term vision of things such that to assure a real technical leadeship.

    Thus, as time goes by, the organization finds itself with a code that was being written with a tunneled vision of the system, by ignoring the wider system and by ignoring what’s coming in the future. There will be no framework in place, no standard practices, each part of the code will be written with different assumptions in mind because each of the developers have different ideas. The modules will not be scalable.Making a change in the code becomes a nightmare and implementing simple features takes a lot longer time than expected. The chickens then begin to wonder why the pigs are unable to deliver at the same speed as in the beginning of the project. The scrum master goes crazy over why the team’s velocity is dipping with each sprint.

     If you're like me, you know that this chickens/pigs paradigma illustrates kind a modern-day-slavery where pigs sacrifice their lives for the project, whereas chickens only have to give up their eggs. This is not only a terminology matter. You can replace these terms by others like players/spectators, contributors/observers, commited/interested, forwards/backs, active/passive, etc., but the deep nature of the dialectic relatyionship stays the same.

     

  • FSW 6.1 – Developing SCA Composites

    JBoss Fuse Service Works (FSW) 6.1 is the new release of the JBoss SOA Platform recently branded by Red Hat. As opposed to the previous releases, like JBoss SOA-P 5.3, which were proprietary solutions based products, FSW 6.1 is an implementation of the OASIS SCA (Service Component Architecture) Assembly specifications (https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=sca-assembly). This article shows a simple practical example of using SCA with FSW 6.1 to implement simple services leveraging different technologies like Apache Camel. But first, let’s look at some basics concepts.

     

    At the core of the SCA specifications is the Assembly Model, i.e. the XML notation to describe the components assembling into applications and to provide the framework into which extensions are plugged such that to support a wide variety of services, implementations and communication technologies. As such, SCA is an essential complement of SOA (Services Oriented Architecture) in the sense that, while SOA advocates the utilization of the enterprise services and defines their characteristics, SCA specifies what a service exactly is from a physical and an assembly point of view.

     

    Everything in SCA is about software components. An SCA component is a configured instance of some business logic. It provides services and it may use services. An SCA service has a name and at least an interface. FSW 6.1 supports Java, WSDL and ESB interfaces for services. Any SCA component defines one or more services. The business logic of these services is provided by their implementations. FSW 6.1 supports CDI (Common Dependency Injection), Camel, BPEL (Business Process Execution Language) and Rules (JBoss Drools) implementations for the services. An SCA service may call other SCA services via service references. This connection between calling and callee services, via references, is called wire in SCA terminology. References are wired to services and so a network of components is described within a composite application. Accordingly, the composite is the basic unit of SCA assembly. The technology used to join the components together in composites is unrelated to the one in which the components are implemented. For example, a Camel based service might be connected with a caller service through HTTP, JMS or SOAP. This abstraction process is called binding in the SCA terminology. FSW 6.1 supports a long list of bindings, like HTTP, JMS, JPA, SOAP, REST, File, FTP/SFTP/FTPS, Camel, TCP/UDP, etc.

     

    Based on the previous notions which should resume in a few sentences the SCA basics and essentials, let’s try now to implement a Hello World composite and to deploy and test it in FSW 6.1. Our Hello World project is an SCA service, having an ESB interface, a Camel implementation and a HTTP binding.

     

    An SCA composite as a FSW project is, essentially, a maven project with a JAR packaging type and, as such, it may be developed using any development tool supporting maven. In this exercise we choose however to use JBoss Development Studio (JBDS) 7.1. SwitchYard is the name of the JBoss open-source project providing the SCA Assembly implementation and it has included as such by Red Hat in their FSW commercial release. Hence, JBDS has a SwitchYard plugin which will be used here for our example purposes. Here are the required steps:

     

    1. Create a new SwitchYard project in JBDS. The New SwithchYard Project wizard will ask you for this new project’s name. A simple naming conversion consists in using a pattern like <name>-<interface>-<implementation>-<binding> for the project name. With the risk of coming to quite long names, this pattern is self-documenting as it specifies the type of the composite service interface, implementation and binding. In our case, this name is helloworld-esb-camel-http. Click Next.
    2. The SwitchYard Project Configuration wizard lets you specify information like the maven group-id, the project’s target name-space the package name for the Java classes, if any, the target runtime, etc. Concerning the target runtime, this supposes that FSW 6.1 be installed locally and that you have created a runtime and an associated server in JBDS. More important, the wizard lets you select the required SwitchYard components. This means that the generated maven POM will only include dependencies for the SwitchYard components you specify at this step. For our example, select Camel Route for Implementation Support, HTTP for Gateway Bindings and HTTP Mixins for Test Mixins. Click Next and your maven POM will be generated.
    3. Now you might want to amend the generated POM, for example, if you use jboss-as:deploy/undeploy or exec:java plugins you need to add the following:

          <plugin>

            <groupId>org.codehaus.mojo</groupId>

            <artifactId>exec-maven-plugin</artifactId>

            <configuration>

              <executable>java</executable>

              <classpathScope>test</classpathScope>

              <mainClass>…</mainClass>

            </configuration>

          </plugin>

          <plugin>

            <groupId>org.jboss.as.plugins</groupId>

            <artifactId>jboss-as-maven-plugin</artifactId>

            <version>7.4.Final</version>

          </plugin>

     

    1. Our service uses a Camel implementation, hence you need to create it. Go to the src/main/java, right-click on the package name that you configured in the Step 2 and create a new class. The following listing shows an example of a Camel Route:

    package …;

    import org.apache.camel.*;
    import org.apache.camel.builder.*;
    import org.switchyard.common.camel.*;

    public class HelloWorldRoute extends RouteBuilder
    {
      @Override
      public void configure() throws Exception
      {
        from("switchyard://HelloWorldESBComponentService")
        .process(new Processor()
        {
          @Override
          public void process(Exchange exchange) throws Exception
          {
            String hello = exchange.getIn().getBody(String.class);
            SwitchYardMessage out = new SwitchYardMessage();
            out.setBody(hello);
            exchange.setOut(out);
          }
        })
        .log("*** ${body}");
      }
    }

    1. We will look more in details later to the code above. Now, that we have the implementation class, we go back to the switchyard.xml file and, in its design tab, drag from the Palette the Camel (Java) icon, located in the Implementations tab, onto the component you created previously. In the Camel Java Route Details click on the Browse button and then select the name of the class you created in the previous step.
    2. Now your component is created with its associated implementation. In order to be used, you need to create a service for it. Drag the Service icon from the Core tab of the Palette onto your component. This will open the New Service wizard. Type in the service name and select the ESB radio button for the Interface Type. Now click on the Inteface link in order to define the input, output and fault types for your ESB interfaces. In our case, we chose to have a java.lang.String type as input and output an no fault type. Click Finish.
    3. Now you have created an SCA component having a service definition associated to it. Your component is accessible via its associated service in the containing composite. But in order to be accessible from outside the composite, another service associated to the composite itself is required. Drag the Service icon from the Core tab of the Palette to the main canvas and repeat the steps you performed at the Step 7 to define the service name and interface. Once this composite level service is created, you need to wire it to your component level service. Move your mouse onto the composite level service and different icons will be displayed around the service. Select the Wire icon, displayed as a bidirectional arrow, and drag it onto the component layer service. The wire will appear on the canvas as a solid line connecting the composite level service to the component level one.
    4.  The idea behind the operation performed at the Step 8 was to have a composite level service to be used in order to access the component level service from outside the composite. In this respect, the SCA terminology calls this operation a service promotion. This means that our component level service is promoted such that to be accessible at the composite level, from outside the composite. Wiring your services will automatically perform the promotion.

     

    Finally, your switchyard.xml file should look similar to the one below. Please notice that the file is not editable in the Source tab of the SwitchYard Editor in JBDS but only in the Design one, using the Palette, as we did. Should you prefer to edit it using XML, you need to use your preferred text editor but not JBDS. Caution should be observed that making mistakes while editing this file with a text editor could prevent you to open it further into JBDS.

     

    <?xml version="1.0" encoding="UTF-8"?>

    <sy:switchyard xmlns:camel="urn:switchyard-component-camel:config:1.1"
      xmlns:http="urn:switchyard-component-http:config:1.1" xmlns:sca="http://docs.oasis-open.org/ns/opencsa/sca/200912"
      xmlns:sy="urn:switchyard-config:switchyard:1.1" name="helloworld-esb-camel-hhtp"
      targetNamespace="urn:com.example.switchyard:helloworld-esb-camel-hhtp:1.0">

      <sca:composite name="helloworld-esb-camel-hhtp"
        targetNamespace="urn:com.example.switchyard:helloworld-esb-camel-hhtp:1.0">

        <sca:service name="HelloWorldESBCompositeService" promote="HelloWorld/HelloWorldESBComponentService">

          <sy:interface.esb inputType="java.lang.String" outputType="java.lang.String"/>

          <http:binding.http>

            <http:contextPath>hello-world2</http:contextPath>

          </http:binding.http>

        </sca:service>

        <sca:component name="HelloWorld">

          <camel:implementation.camel>

            <camel:java class="fr.simplex_software.switchyard.HelloWorldRoute"/>

          </camel:implementation.camel>

          <sca:service name="HelloWorldESBComponentService">

            <sy:interface.esb inputType="java.lang.String" outputType="java.lang.String"/>

          </sca:service>

        </sca:component>

      </sca:composite>

    </sy:switchyard>

     

    Using an editor as the SwitchYard one in JBDS avoids you having to learn the XML notations like the previous one. Here we define an SCA component named HelloWorld implemented by a Camel route defined by the HelloWorldRoute Java class. There is a component level service associated to this component. Its name is HelloWorldComponentService and its interface is of ESB type accepting a string as its input and output. The implemnation class uses a Camel route which reads the input string. Please notice that, for this purposes, the Camel endpoint is switchyard://HelloWorldESBComponentService. A Camel processor is then used to copy the Camel payload to the Camel output which will be the service output as well. This output string will be finally logged in the FSW log file. The component level service is promoted to a composite level service named HelloWorldCompositeService. This is a useful naming convention to use ComponentService and CompositeService suffixes for the services names. Please notice how the services promotion works.

     

    Last but not least, we need a test class like the following:

    package …;

     

    import org.switchyard.component.test.mixins.http.*;

     

    public class TestHelloWorld

    {

      private static final String HELLO_WORLD_URL = "http://localhost:8080/hello-world2";

     

      public static void main(String[] args) throws Exception

      {

        HTTPMixIn http = new HTTPMixIn();

        http.initialize();

        http.sendString(HELLO_WORLD_URL, "Hello World !", HTTPMixIn.HTTP_POST);

      }

    }

     

    SwitchYard comes with a test framework called Mixin. This framework comes in different flavours like CDI, HTTP, HornetQ, etc. Here since our service has an HTTP binding, we’re using the HTTP flavor for Mixin. As you may see, once instantiated and initialized, the HTTPMixIn class may be used to send HTTP request to HTTP endpoints. So, here we’re using an HTTP transport in order to send the input string to our service implemented by a Camel route.

     

    In order to deploy our project and to test it, after having started the FSW server, use the following command:

     

    mvn clean install jboss-as:undeploy jboss-as:deploy exec:java

     

    The maven command above will clean the previously created files, will compile, package and install the JAR into the local maven repository, it will also undeploy the JAR from the FSW server if it is already deployed and it will deploy it again on the FSW server. Finally the test class will be executed. If everything goes well, you should see the following message in the FSW log file.

    12:32:36,722 INFO  [route4] (http-localhost/127.0.0.1:8080-1) *** Hello World !

     

    The example could be downloaded from the download page of this site. Enjoy and don’t hesitate to let me know your comments.