Aplicación de ejemplo usando varias especificaciones de Java EE 7

Escrito por picodotdev el , actualizado el .
blog-stack java planeta-codigo programacion
Comentarios

Más lentamente que otras tecnologías Java EE en cada nueva versión sigue adaptándose a las nuevas tendencias en el desarrollo y facilitando la programación de aplicaciones de entidad empresarial. Java EE está formado por un conjunto de especificaciones que resuelven en gran medida muchas de las necesidades funcionales de las aplicaciones ya sean de persistencia, seguridad, mensajería, lógica de negocio, transaccionalidad, inyección de dependencias, presentación HTML, JSON, WebSockets, conexión a base de datos, envío de correos electrónicos o concurrencia. En este artículo mostraré un ejemplo usando varias de estas especificaciones y proporcionaré el código fuente completo.

Java EE
Java

Comentaba las novedades y nuevas características de Java EE 7 que en el 2013 trajo esta nueva versión para el desarrollo de aplicaciones empresariales con el lenguaje Java. Hacía una descripción y cuáles eran las especificaciones y versiones de las mismas que proporcionan la funcionalidades comunes a muchas aplicaciones. En este artículo mostraré un ejemplo con código con la implementación de una aplicación usando varias de las especificaciones de Java EE.

El ejemplo consistirá en una supuesta aplicación sencilla para hacer la lista de la compra de un supermercado usando las siguientes especificaciones JSF, JPA, JTA, Security, CDI, EJB, REST, JAX-RS, JSON, eventos CDI y WebSockets. El comprador irá seleccionando productos y la cantidad de los mismos de su compra, la aplicación le informará del precio de los productos seleccionados hasta el momento y finalmente para hacer la compra iniciará sesión. En el momento que se realice una compra el stock de productos se actualizará en todos los clientes conectados al supermercado.

Este ejemplo está relacionado con otros artículos que he escrito como Ejemplo de multiproyecto con Gradle, Novedades y nuevas características de Java 8 y alguno quizá considere de la misma forma que sigue habiendo más de 10 razones para seguir usando Java.

Para facilitar el desarrollo de la aplicación en el lado cliente usaré algunas librerías JavaScript de forma similar al ejemplo de la lista de tareas con Backbone y React junto con RequireJS. Seleccionados los productos, la compra se realizará usando una API REST intercambiando los datos con formato JSON, se persistirá en la base de datos con JPA actualizándose en ese momento el stock de los productos de lo que se encargará un EJB para controlar esta pequeña lógica de negocio, si no hay stock suficiente de un producto se producirá una excepción y devolverá un código de estado adecuado en la petición HTTP, al realizar la compra se generará un mensaje evento CDI que desencadenará la actualización del stock de los usuarios conectados usando WebSockets. La página del listado de productos y el formulario de autenticación se generará usando la tecnología de presentación JSF.

Supermarket con Java EE 7

Veamos primero la página inicial índice con el listado de productos generada con JSF. Obtiene el listado de productos y genera el HTML del mismo, además carga los JavaScripts necesarios para que la aplicación funcione en el navegador del cliente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<body>
<ui:composition template="template.xhtml">
     <ui:define name="script">
         <script type="text/javascript" src="javascript/main-index.js"></script>
     </ui:define>
     <ui:define name="content">
          <div class="row">
               <div class="col-md-12">
                    <ul id="products" class="list-unstyled">
                         <c:forEach var="product" items="#{indexBean.products}" varStatus="status">
                              <li class="col-md-3" data-id="#{product.id}" data-stock="#{product.stock}" data-price="#{product.price}" data-amount="#{indexBean.getAmount(product)}">
                                   <div class="media">
                                        <div class="media-left">
                                             <a href="#">
                                                  <img data-holder-rendered="true" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PCEtLQpTb3VyY2UgVVJMOiBob2xkZXIuanMvNjR4NjQKQ3JlYXRlZCB3aXRoIEhvbGRlci5qcyAyLjYuMC4KTGVhcm4gbW9yZSBhdCBodHRwOi8vaG9sZGVyanMuY29tCihjKSAyMDEyLTIwMTUgSXZhbiBNYWxvcGluc2t5IC0gaHR0cDovL2ltc2t5LmNvCi0tPjxkZWZzPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PCFbQ0RBVEFbI2hvbGRlcl8xNTM3NmFhMTMwZSB0ZXh0IHsgZmlsbDojQUFBQUFBO2ZvbnQtd2VpZ2h0OmJvbGQ7Zm9udC1mYW1pbHk6QXJpYWwsIEhlbHZldGljYSwgT3BlbiBTYW5zLCBzYW5zLXNlcmlmLCBtb25vc3BhY2U7Zm9udC1zaXplOjEwcHQgfSBdXT48L3N0eWxlPjwvZGVmcz48ZyBpZD0iaG9sZGVyXzE1Mzc2YWExMzBlIj48cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSIxNC41IiB5PSIzOCI+NjR4NjQ8L3RleHQ+PC9nPjwvZz48L3N2Zz4=" style="width: 64px; height: 64px;" class="media-object" data-src="holder.js/64x64" alt="64x64"/>
                                             </a>
                                        </div>
                                        <div class="media-body">
                                             <h4 class="media-heading">
                                                  #{product.name}
                                             </h4>
                                             <span class="label label-primary stock" title="#{product.stock} in stock">#{product.stock}</span>
                                             <span class="label label-success">#{product.price} €</span>
                                             <span class="label label-default amount" title="#{indexBean.getAmount(product)} in cart">#{indexBean.getAmount(product)}</span>
                                             <button type="button" class="btn btn-primary btn-xs add" title="Add to cart"><span class="glyphicon glyphicon-plus"></span></button>
                                             <button type="button" class="btn btn-primary btn-xs subtract" title="Subtract from cart"><span class="glyphicon glyphicon-minus"></span></button>
                                        </div>
                                   </div>
                              </li>
                         </c:forEach>
                    </ul>
                </div>
           </div>
           <div class="row">
               <div class="col-md-12 text-center">
                    <div id="purchasePrice" data-price="0,00" class="total-price">#{indexBean.getPrice()} €</div>
                    <c:choose>
                         <c:when test="#{indexBean.buyer}">
                              <button id="buy" type="button" class="btn btn-primary btn-lg">Buy</button>
                         </c:when>
                         <c:otherwise>
                              <h:link value="Login to buy" outcome="login" class="btn btn-primary btn-lg"/>
                         </c:otherwise>
                    </c:choose>
               </div>
           </div>
     </ui:define>
</ui:composition>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<h:head>
    <title><ui:insert name="title">Supermarket</ui:insert></title>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <link type="text/css"  href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.css" rel="stylesheet"/>
    <h:outputStylesheet name="css/main.css" />
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"></script>
    <script type="text/javascript" src="javascript/requirejs-config.js"></script>
    <ui:insert name="script"></ui:insert>
</h:head>
<h:body>
    <div class="container">
         <div class="row">
             <div class="col-md-12">
                  <ui:insert name="top">
                       <div class="jumbotron">
                           <h:graphicImage value="image/java-ee.png" alt="Java EE 7" title="Java EE 7"/>
                           <h1>Supermarket with Java EE 7</h1>
                            <p>#{indexBean.products.size()} products.</p>
                       </div>
                  </ui:insert>
             </div>
         </div>
         <div class="row">
        <div class="col-md-12">
             <ui:insert name="content"></ui:insert>
        </div>
         </div>
    </div>
</h:body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package io.github.picodotdev.blogbitix.javaee7.beans;

import io.github.picodotdev.blogbitix.javaee.ejb.SupermarketLocal;
import io.github.picodotdev.blogbitix.javaee.jpa.Cart;
import io.github.picodotdev.blogbitix.javaee.jpa.Product;

import javax.ejb.EJB;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Named
@RequestScoped
public class IndexBean {

    @EJB
    private SupermarketLocal supermarket;

    public List<Product> getProducts() {
        return supermarket.findProducts();
    }

    public Cart getCart() {
        Cart cart = (Cart) FacesContext.getCurrentInstance().getExternalContext().getSessionMap().get("cart");
        if (cart == null) {
            cart = Cart.EMPTY;
        }
        return cart;
    }

    public boolean isBuyer() {
        return FacesContext.getCurrentInstance().getExternalContext().isUserInRole("buyer");
    }

    public Integer getAmount(Product product) {
        for (Map<String, String> item : getCart().getItems()) {
            Long id = Long.parseLong(item.get("id"));
            if (id.equals(product.getId())) {
                return Integer.parseInt(item.get("amount"));
            }
        }
        return 0;
    }

    public BigDecimal getPrice() {
        BigDecimal price = new BigDecimal("0");
        for (Map<String, String> item : getCart().getItems()) {
            Long id = Long.parseLong(item.get("id"));
            Product product = supermarket.findProduct(id);
            Integer amount = Integer.parseInt(item.get("amount"));
            price = price.add(product.getPrice().multiply(new BigDecimal(amount)));
        }
        return price;
    }
}

El JavaScript añade la lógica en el cliente para ir realizando la lista de la compra usando poco más que jQuery y Require JS, además, inicializa el WebSocket para recibir los mensajes desde el servidor con la actualizaciones del stock de los productos. Realizada la lista de productos se enviará un petición REST al servidor para formalizar la compra.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
require(['jquery', 'underscore'], function($, _) {
    $('#products').on('click', 'li button.add', function() {
      var element = $(this).closest('li');
      var amount = parseInt(element.attr('data-amount'));
      amount += 1;
      element.attr('data-amount', amount);
      element.find('span.amount').html(amount);

      updatePrice();
      updateCart();
    });
    $('#products').on('click', 'li button.substract', function() {
      var element = $(this).closest('li');
      var amount = parseInt(element.attr('data-amount'));
      amount -= 1;
      amount = (amount < 0) ? 0 : amount;
      element.attr('data-amount', amount);
      element.find('span.amount').html(amount);

      updatePrice();
      updateCart();
    });

    $('#buy').on('click', function() {
         var json = JSON.stringify(getCart());

         $.ajax({
             url: 'https://localhost:8443/war/rest/purchases',
             type: 'POST',
             dataType: 'text',
             data: json,
             contentType: 'application/json',
             success: function(response) {
             },
             error: function(err) {
             }
        });
    });

    function updateCart() {
         var json = JSON.stringify(getCart());

         $.ajax({
             url: 'https://localhost:8443/war/rest/cart',
             type: 'POST',
             dataType: 'text',
             data: json,
             contentType: 'application/json',
             success: function(response){
             },
             error: function(err){
             }
        });
    }

    function updatePrice() {
        var total = 0;
        $.each($('li', '#products'), function() {
            var price = parseFloat($(this).attr('data-price'));
            var amount = parseInt($(this).attr('data-amount'));
            total += price * amount;
        });
        total = total.toFixed(2);
        var purchasePrice = $('#purchasePrice');
        purchasePrice.attr('data-price', total);
        purchasePrice.html(total + ' €');
    }

    function getCart() {
         var cart = {items: []};
         $.each($('li', '#products'), function() {
              var id = parseInt($(this).attr('data-id'));
              var amount = parseInt($(this).attr('data-amount'));

              if (amount > 0) {
                  cart.items.push({id: id, amount: amount});
              }
         });
         return cart;
    }

    // WebSocket
    var stocks = new WebSocket("wss://localhost:8443/war/stock");
    stocks.onopen = function() {
       stocks.send('ping');
    }

    stocks.onerror = function(evt) {

    }

    stocks.onclose = function() {

    }

    stocks.onmessage = function(message) {
        console.log(message);
        _.each(JSON.parse(message.data), function(it) {
            var element = $('li[data-id="' + it.id + '"]', '#products');
            element.attr('data-stock', it.stock);
            element.find('span.stock').attr('title', it.stock + ' in stock').html(it.stock);
        });
    }

    $(window).on('beforeunload', function(){
        stocks.close();
    });
});

Las peticiones de compras en el servidor se procesarán por un endpoint de una interfaz REST que usando un EJB con la lógica de negocio para persistir la compra en una base de datos relacional y actualizar los stocks. Actualizados los stocks y persistida la compra se genera un evento CDI con el hecho de que se ha producido una compra.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package io.github.picodotdev.blogbitix.javaee7.rest;

import io.github.picodotdev.blogbitix.javaee.ejb.SupermarketLocal;
import io.github.picodotdev.blogbitix.javaee.jpa.Cart;
import io.github.picodotdev.blogbitix.javaee.jpa.NoStockException;
import io.github.picodotdev.blogbitix.javaee.jpa.Purchase;
import io.github.picodotdev.blogbitix.javaee.jpa.User;

import javax.annotation.security.RolesAllowed;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import java.util.List;

@Path("purchases")
@RequestScoped
public class PurchasesResource {

    @EJB
    private SupermarketLocal supermarket;

    @Context
    SecurityContext securityContext;

    @GET
    @Produces({ MediaType.APPLICATION_JSON })
    @RolesAllowed({ "buyer" })
    public List<Purchase> list() {
        return supermarket.findPurchases();
    }

    @GET
    @Path("{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    @RolesAllowed({ "buyer" })
    public Purchase get(@PathParam("id") Long id) {
        return supermarket.findPurchase(id);
    }

    @POST
    @Consumes({ MediaType.APPLICATION_JSON })
    @RolesAllowed({ "buyer" })
    public Purchase post(Cart cart) throws NoStockException {
        User user = supermarket.findUser(securityContext.getUserPrincipal().getName());
        return supermarket.buy(cart, user);
    }
}
1
2
3
4
5
6
7
8
package io.github.picodotdev.blogbitix.javaee7.rest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class ApplicationConfig extends Application {
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.javaee7.rest;

import io.github.picodotdev.blogbitix.javaee.jpa.NoStockException;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class NoStockExceptionHandler implements ExceptionMapper<NoStockException> {

    @Override
    public Response toResponse(NoStockException exception) {
        return Response.status(Response.Status.FORBIDDEN).entity(exception.getMessage()).build();
    }
}

La aplicación irá registrando los usuarios conectados a la aplicación con el objetivo de enviarles las actualizaciones de stock de los productos y con un evento CDI con la notificación de que se ha producido una compra que ha actualizado el stock de algunos productos enviará los nuevos stocks a los clientes con la tecnología WebSocket en un mensaje con datos en JSON que soportan los navegadores y Java EE.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
package io.github.picodotdev.blogbitix.javaee.ejb;

import io.github.picodotdev.blogbitix.javaee.jpa.*;

import javax.ejb.AccessTimeout;
import javax.ejb.Stateless;
import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Stateless
@AccessTimeout(value = 15, unit = TimeUnit.SECONDS)
public class Supermarket implements SupermarketLocal, SupermarketRemote {

    @Inject @Any
    private Event<Purchase> event;

    @PersistenceContext(name = "primary")
    private EntityManager em;

    @Override
    public List<Product> findProducts() {
        TypedQuery<Product> query = em.createQuery("SELECT p FROM Product p", Product.class);
        return query.getResultList();
    }

    @Override
    public Product findProduct(Long id) {
        TypedQuery<Product> query = em.createQuery("SELECT p FROM Product p where id = :id", Product.class);
        query.setParameter("id", id);
        return query.getSingleResult();
    }

    @Override
    public User findUser(String name) {
        TypedQuery<User> query = em.createQuery("SELECT u FROM User u where name = :name", User.class);
        query.setParameter("name", name);
        return query.getSingleResult();
    }

    @Override
    @Transactional
    public void persistsProduct(Product product) {
        em.persist(product);
    }

    @Override
    @Transactional
    public void deleteProduct(Product product) {
        em.remove(product);
    }

    @Override
    public List<Purchase> findPurchases() {
        TypedQuery<Purchase> query = em.createQuery("SELECT p FROM Purchase p", Purchase.class);
        return query.getResultList();
    }

    @Override
    public Purchase findPurchase(Long id) {
        TypedQuery<Purchase> query = em.createQuery("SELECT p FROM Purchase p where id = :id", Purchase.class);
        query.setParameter("id", id);
        return query.getSingleResult();
    }

    @Override
    @Transactional
    public Purchase buy(Cart cart, User user) throws NoStockException {
        List<Item> items = cart.getItems().stream().map(i -> {
            Long id = Long.parseLong(i.get("id"));
            Integer amount = Integer.parseInt(i.get("amount"));

            Item item = new Item();
            item.setProduct(findProduct(id));
            item.setAmount(amount);

            return item;
        }).collect(Collectors.toList());

        Purchase purchase = new Purchase();
        purchase.setDate(new Date());
        purchase.setBuyer(user);
        purchase.setItems(items);

        List<Item> withoutStock = purchase.getItems().stream().filter(item -> !item.getProduct().hasStock(item.getAmount())).collect(Collectors.toList());
        if (!withoutStock.isEmpty()) {
            throw new NoStockException(withoutStock);
        }
        for (Item item : purchase.getItems()) {
            item.getProduct().subtractStock(item.getAmount());
        }

        em.persist(purchase);
        event.fire(purchase);

        return purchase;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.github.picodotdev.blogbitix.javaee.ejb;

import io.github.picodotdev.blogbitix.javaee.jpa.*;

import javax.ejb.Local;
import java.util.List;

@Local
public interface SupermarketLocal {

    List<Product> findProducts();

    Product findProduct(Long id);

    User findUser(String name);

    void persistsProduct(Product product);

    void deleteProduct(Product product);

    List<Purchase> findPurchases();

    Purchase findPurchase(Long id);

    Purchase buy(Cart cart, User buyer) throws NoStockException;
}

Usando la API de seguridad de Java EE autenticaremos al comprador o vendedor, la página se personalizará según el rol del usuario y en el servidor con la anotación RolesAllowed y métodos post, get, list se limitarán las acciones que pueden realizar según sus roles, su uso se puede ver en los listados de código anteriores. Con la página de inicio de sesión se autenticará al usuario de forma programática usando request.login(username, password);. Esta acción es recomendable hacerla usando el protocolo seguro HTTPS con TLS a configurar en el servidor para que la contraseña se transmita cifrada entre el cliente y el servidor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<body>
<ui:composition template="template.xhtml">
     <ui:define name="top">
          <div class="jumbotron">
               <h1>Supermarket with Java EE 7</h1>
          </div>
     </ui:define>
     <ui:define name="content">
          <h:form class="form-horizontal">
               <div class="form-group">
                    <h:outputLabel for="username" class="col-sm-2 control-label">Username</h:outputLabel>
                    <div class="col-sm-10">
                         <h:inputText id="username" value="#{loginBean.username}" class="form-control"></h:inputText>
                    </div>
               </div>
               <div class="form-group">
                    <h:outputLabel for="password" class="col-sm-2 control-label">Password</h:outputLabel>
                    <div class="col-sm-10">
                        <h:inputSecret id="password" value="#{loginBean.password}" autocomplete="off" class="form-control"></h:inputSecret>
                    </div>
               </div>
               <div class="form-group">
                    <div class="col-sm-offset-2 col-sm-10">
                        <h:commandButton value="Login" action="#{loginBean.login}" class="btn btn-primary" />
                    </div>
                </div>
          </h:form>
     </ui:define>
</ui:composition>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package io.github.picodotdev.blogbitix.javaee7.beans;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;

@ManagedBean
@RequestScoped
public class LoginBean implements Serializable {

    private String username;
    private String password;

    @Inject
    private HttpServletRequest request;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String login() {
        try {
            request.login(username, password);

            FacesContext context = FacesContext.getCurrentInstance();
            HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
            response.sendRedirect("index.xhtml");
            return null;
        } catch (Exception e) {
            return "login.xhtml?e=1";
        }
    }
}

La aplicación está dividida en varios módulos construidos con la herramienta de automatización Gradle siendo una aplicación EAR estándar estando constituida por un módulo para los EJB, otro para la aplicación web con un WAR. Un cliente podría conectarse directamente a la aplicación sin mediación de un navegador web, esto último sería lo que emplearíamos si fuese una aplicación de escritorio empleando Java FX.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package io.github.picodotdev.blogbitix.javaee.jar;

import io.github.picodotdev.blogbitix.javaee.ejb.SupermarketRemote;
import io.github.picodotdev.blogbitix.javaee.jpa.Product;
import io.github.picodotdev.blogbitix.javaee.jpa.Purchase;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.stream.Collectors;

public class SupermarketClient {

    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        SupermarketRemote supermarket = (SupermarketRemote) context.lookup("ejb:ear/ejb/Supermarket!io.github.picodotdev.blogbitix.javaee.ejb.SupermarketRemote");

        List<Product> products = supermarket.findProducts();
        List<Purchase> purchases = supermarket.findPurchases();

        printProductsSummary(products);
        printPurchasesSummary(purchases);
    }

    private static void printProductsSummary(List<Product> products) {
        BigDecimal maxPriceProduct = products.stream().map(a -> {
            return a.getPrice();
        }).sorted((a, b) -> {
            return b.compareTo(a);
        }).findFirst().orElse(new BigDecimal("0"));
        BigDecimal minPriceProduct = products.stream().map(a -> {
            return a.getPrice();
        }).sorted((a, b) -> {
            return a.compareTo(b);
        }).findFirst().orElse(new BigDecimal("0"));
        BigDecimal sumPriceProduct = products.stream().map(p -> p.getPrice()).collect(Collectors.reducing(new BigDecimal("0"), (result, element) -> {
            return result.add(element);
        }));
        BigDecimal avgPriceProduct = (products.size() == 0) ? new BigDecimal("0") : sumPriceProduct.divide(new BigDecimal(products.size()), RoundingMode.HALF_UP);
        Double avgStockProduct = products.stream().collect(Collectors.averagingDouble(p -> {
            return p.getStock();
        }));
        System.out.printf("Products summary(count: %d, maxPrice: %.2f, minPrice: %.2f, avgPrice: %.2f, avgStock: %.2f%n", products.size(), maxPriceProduct, minPriceProduct,
                avgPriceProduct, avgStockProduct);
    }

    private static void printPurchasesSummary(List<Purchase> purchases) {
        Integer numProducts = purchases.size();
        BigDecimal maxPriceProduct = purchases.stream().map(a -> {
            return a.getPrice();
        }).sorted((a, b) -> {
            return b.compareTo(a);
        }).findFirst().orElse(new BigDecimal("0"));
        BigDecimal minPriceProduct = purchases.stream().map(a -> {
            return a.getPrice();
        }).sorted((a, b) -> {
            return a.compareTo(b);
        }).findFirst().orElse(new BigDecimal("0"));
        BigDecimal sumPriceProduct = purchases.stream().map(p -> p.getPrice()).collect(Collectors.reducing(new BigDecimal("0"), (result, element) -> {
            return result.add(element);
        }));
        BigDecimal avgPriceProduct = (purchases.size() == 0) ? new BigDecimal("0") : sumPriceProduct.divide(new BigDecimal(numProducts), RoundingMode.HALF_UP);
        System.out.printf("Purchases summary(count: %d, maxPrice: %.2f, minPrice: %.2f, avgPrice: %.2f", purchases.size(),
                maxPriceProduct, minPriceProduct, avgPriceProduct);
    }
}
Aplicación cliente Supermarket

El conjunto de especificaciones de Java EE proporciona una solución para la mayoría de funcionalidades que necesita una aplicación pero también podemos sustituir alguna y combinarlas con otras de las muchas librerías o frameworks disponibles en la plataforma Java. Por ejemplo, como framework en vez de usar Servlet y JSP o JSF podemos usar Apache Tapestry, Vert.x, Spark, Struts, Grails, … dependiendo de las necesidades de la aplicación, su complejidad y nuestras preferencias. Como alternativa al ORM de JPA o JDBC se puede usar jOOQ. RabbitMQ en vez de JMS o Spring en vez de CDI, EJB y JTA.

Algunos libros sobre Java EE que he leído y que me han gustado han sido los siguientes, Java EE 7 Essentials hace un repaso detallado pero no muy profundo para hacerse una idea bastante buena del conjunto de especificaciones de Java EE. Otro buen libro es Java EE 7 Development with WildFly que entra más en detalle en cada una de las especificaciones. El tutorial oficial de Java EE 7 también es un buen punto de partida. Libros sobre Java EE 7 hay muchos por la cantidad de tiempo que ya tiene, es recomendable leer alguno que esté actualizado a las últimas versiones.

Con Java EE 6 y 7 ciertas partes de configuración se pueden realizar con anotaciones en vez de con XML sin embargo aún no ha desaparecido completamente aunque si reducido considerablemente.

Ejecutar este ejemplo require instalar previamente el servidor de aplicaciones WildFly e iniciarlo con ./standalone.sh -c standalone-full.xml. Iniciado el servidor y desplegada la aplicación con el siguiente comando de Gradle se puede acceder a ella con el navegador en la dirección https://localhost:8443/war/ teniendo el protocolo seguro configurado.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew build deploy.

En el futuro Java EE 8 está planificado un framework basado en acciones en vez de componentes como en JSF, también el soporte para el protocolo HTTP/2.