miércoles, 23 de noviembre de 2016

Vaadin 7 + Grails 3.2 Plugin Spring-Boot

Para aquellos amantes de aplicaciones Grails, pero que desean un toque de UI con Vaadin pueden usar de lleno el plugin que propicia oficialmente Vaadin en su web que se integra a Spring y Spring-Boot.

Un proyecto completo y funcional con solo el Vaadin plugin oficial, un adicional para compilar widgets y otros pequeños elementos de configuración de Grails es todo lo que necesitaremos.

Demo Git Repositorio grails3vaadinspringboot


Como se hace?


Lo primero que debemos hacer es iniciar un proyecto Grails (La demo es 3.2.3) como de costumbre en nuestro IDE preferido (IntelliJ fue el usado en mi caso)

Una vez creado pasamos a configurar nuestro build.gradle para las dependencias deseadas con las siguientes instrucciones:

  • Agregamos esto para configurar un caso de Vaadin que da problemas con el hibernate
    configurations {
        //noinspection GroovyAssignabilityCheck
        'vaadin-client' {
            resolutionStrategy.force "javax.validation:validation-api:1.0.0.GA"
            exclude module: 'spring-boot-starter-web'
            exclude module: 'spring-boot-vaadin'
        }
    }
    
    
  • Adicionamos los repositorios tantos a el caso normal como el buildscript
    maven { url "https://repo.spring.io/libs-release" }
    maven { url "http://maven.vaadin.com/vaadin-addons" }
    maven { url "https://plugins.gradle.org/m2/" }
    
    
  • Adicionamos un Boom para que siempre busque tope en las dependencias
    mavenBom "com.vaadin:vaadin-bom:$vaadinVersion"
    
    
  • Adicionamos las dependencias deseadas (Pueden agregar los plugins si desean)
    compile "com.vaadin:vaadin-spring-boot-starter:$vaadinSpringVersion"
    compile "com.vaadin:vaadin-spring-boot:$vaadinSpringVersion"
    compile "com.vaadin:vaadin-push:$vaadinVersion"
    
    
  • Una vez aqui solo falta configurar un plugin en el classpath para poder compilar los elementos de Vaadin
    
    classpath "fi.jasoft.plugin:gradle-vaadin-plugin:$vaadinGradleVersion"
    
    
  • Configuramos el plugin extra para que nos trabaje con los elementos deseados.vaadin {
    //noinspection GroovyAssignabilityCheck
    version "$vaadinVersion"
    widgetset 'AppWidgetSet'
    vaadinTestbench.enabled = false
    }
  • Ahora activamos el plugin para que trabaje.
    apply plugin: fi.jasoft.plugin.GradleVaadinGroovyPlugin
  • Por Ultimo refrescamos las dependencias y vemos que se descarguen todos los elementos y configure correctamente.

En fin el resultado del archivo tiene que ser este (Conste que la versión de Grails influye en las dependencias):

import fi.jasoft.plugin.GradleVaadinGroovyPlugin

buildscript {
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
        maven { url "https://repo.spring.io/libs-release" }
        maven { url "http://maven.vaadin.com/vaadin-addons" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.11.6"
        classpath "org.grails.plugins:hibernate5:6.0.4"
        classpath "fi.jasoft.plugin:gradle-vaadin-plugin:$vaadinGradleVersion"
    }
}

version "1.0.0.0"
group "grails3vaadinspringboot"

apply plugin: "eclipse"
apply plugin: "idea"
apply plugin: "war"
apply plugin: 'groovy' // Need it
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.grails-gsp"
apply plugin: "asset-pipeline"
apply plugin: GradleVaadinGroovyPlugin

configurations {
    //noinspection GroovyAssignabilityCheck
    'vaadin-client' {
        resolutionStrategy.force "javax.validation:validation-api:1.0.0.GA"
        exclude module: 'spring-boot-starter-web'
        exclude module: 'spring-boot-vaadin'
    }
}

repositories {
    mavenLocal()
    maven { url "https://repo.grails.org/grails/core" }
    maven { url "https://repo.spring.io/libs-release" }
    maven { url "http://maven.vaadin.com/vaadin-addons" }
    maven { url "https://plugins.gradle.org/m2/" }
}

dependencyManagement {
    imports {
        mavenBom "org.grails:grails-bom:$grailsVersion"
        mavenBom "com.vaadin:vaadin-bom:$vaadinVersion"
    }
    applyMavenExclusions false
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-logging"
    compile "org.springframework.boot:spring-boot-autoconfigure"
    compile "org.grails:grails-core"
    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.grails:grails-dependencies"
    compile "org.grails:grails-web-boot"
    compile "org.grails.plugins:cache"
    compile "org.grails.plugins:scaffolding"
    compile "org.grails.plugins:hibernate5"
    compile "org.hibernate:hibernate-core:5.1.2.Final"
    compile "org.hibernate:hibernate-ehcache:5.1.2.Final"
    console "org.grails:grails-console"
    profile "org.grails.profiles:web"
    runtime "com.bertramlabs.plugins:asset-pipeline-grails:2.11.6"
    runtime "com.h2database:h2"
    testCompile "org.grails:grails-plugin-testing"
    testCompile "org.grails.plugins:geb"
    testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
    testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"

    compile "com.vaadin:vaadin-spring-boot-starter:$vaadinSpringVersion"
    compile "com.vaadin:vaadin-spring-boot:$vaadinSpringVersion"

    compile "com.vaadin:vaadin-push:$vaadinVersion"

    // Vaadin plugins
    compile("org.vaadin.addons.lazyquerycontainer:vaadin-lazyquerycontainer:7.6.1.3") {
        transitive = false
    }
    compile "org.vaadin.addons:fontawesomelabel:1.3.4"
}

bootRun {
    jvmArgs = ['-Dspring.output.ansi.enabled=always']
}

assets {
    minifyJs = true
    minifyCss = true
}

vaadin {
    //noinspection GroovyAssignabilityCheck
    version "$vaadinVersion"
    widgetset 'AppWidgetSet'
    vaadinTestbench.enabled = false
}


Con esto realizado el proyecto funciona?, No!, aun nos faltan unos elementos

Dentro del proyecto realizaremos unos cambios menores para hacer que Grails nos ejecute el caso de Vaadin adicionando un decorador a el main de la aplicación que se encuentra en grails-ppp y en la carpeta init "Application.groovy"

@ComponentScan(["my.package.one", "my.package.two"])

Debe quedar así

package grails3vaadinspringboot

import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import org.springframework.context.annotation.ComponentScan

@ComponentScan(["grails3vaadinspringboot", "com.alsnightsoft.vaadin.demo"])
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}

Una vez hecho lo anterior nos falta crear el Bean que configura a Vaadin en nuestro Grails, ya como dimos los paquetes que usaremos en nuestro proyecto solo debemos crear una clase en donde nuestro ComponentScan la encuentre y esta debe quedar así:

package com.alsnightsoft.vaadin.demo.utils.configurations

import com.vaadin.spring.annotation.EnableVaadin
import com.vaadin.spring.server.SpringVaadinServlet
import groovy.transform.CompileStatic
import org.springframework.boot.web.servlet.ServletRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
 *  Created by aluis on 7/4/16.
 */
@SuppressWarnings("GroovyUnusedDeclaration")
@Configuration
@EnableVaadin
@CompileStatic
class VaadinConfiguration {

    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        return new ServletRegistrationBean(new SpringVaadinServlet(), "/*", "/VAADIN/*")
    }
}

Ahora que ya nuestro proyecto encontrará la configuración que nos falta configuraremos la UI quedándonos como ejemplo esta:

import com.vaadin.annotations.Push
import com.vaadin.annotations.Theme
import com.vaadin.annotations.Widgetset
import com.vaadin.server.VaadinRequest
import com.vaadin.spring.annotation.SpringUI
import com.vaadin.ui.Label
import com.vaadin.ui.UI
import groovy.transform.CompileStatic

/**
 *  Created by aluis on 11/22/16.
 */
@Push
@Theme("valo")
@Widgetset("AppWidgetSet")
@SpringUI(path = "/")
@CompileStatic
class DemoUI extends UI {

    @Override
    protected void init(VaadinRequest vaadinRequest) {
        setContent(new Label("Work!"))
    }
}

Por fin nuestro proyecto esta listo para arrancar, pero puede darnos unos pequeños problemas con los widgets (puede que con el theme sino sabemos configurarlo), pero tranquilos!, solo es correr 2 simples comandos de Gradle para que nos agregue el AppWidgetSet y los compile que son los siguientes (usando IntelliJ solo vamos a la pestaña de Gradle y en Vaadin le damos doble click a esos métodos)


  1. vaadinUpdateWidgetset
  2. vaadinCompile

Con esto corremos el proyecto en BootRun o en modo Grails y tendremos un poco para jugar. En el demo se usan algunas clases auxiliares para el patrón I18N y dar paso a los servicios, así como la implementación del multi idioma.

domingo, 27 de marzo de 2016

Email en aplicaciones Java (Con MailGun)

Nuestras aplicaciones tienen como objetivo facilitar tareas pesadas y tediosas al usuario maximizando la producción y minimizando costos, tiempo y personal, pero también es necesario entregar información al usuario en reportes y enviarle notificaciones a un sitio respectivamente confiable que el usuario pueda valerse y controlar para visualizar dicha información.

Para estos casos recurrimos al correo electrónico, pero en cuanto a desarrollo aveces nos presenta un desafío ya no sabemos configurarlo como es debido y tendemos a caer en spam o incluso a entrar a las listas negras por no tener un control de como se envían los correos.

Debido a algunos problemas presentados por amigos y colegas con respecto a este tema les mostraré como configurar un servicio de correo para nuestras aplicaciones.

Nota: Debido a que el servidor donde corre la aplicación debe tener servicio de correo (Software para email) obviaré esta parte y solo presentaré la configuración del servicio en cuanto al punto del desarrollador, por lo que daré por sentado que en el servidor ya se tiene configurado un servicio de correo (Consta decir que lo que mostraré a continuación es la forma de exponer nuestro servicio de correo al exterior). También daré por sentado que saben configurar los nombres de dominios en sus respectivos servicios.

Ahora bien, para poder configurar y enviar los correos en nuestra aplicación existen ciertos factores a tomar en cuenta y lo planteo con las siguientes preguntas:
  • Que servicio de correo voy a usar?.
  • Existe un API para trabajar con el servicio que seleccione?
  • Como enviaremos el correo?
  • Como creo el contenido del correo?
Entonces procedemos a resolver el caso respondiendo las preguntas

  • MailGun será nuestro servicio.
  • Podría encontrarse un API, pero es posible programar el asunto sin mucho juego y de forma sencilla.
  • Usaremos las librerías que nos aconseja MailGun
  • Para esto dispondremos de ciertas herramientas para hacer más elegante el correo. (Templates responsive)


Una vez definido procedemos a crear la cuenta en el servicio (nuestro caso MailGun). Una vez nos registramos tenemos que configurar un dominio o si prefieren para probar se puede usar el que viene al crear la cuenta.

Esta claro que si usaremos un dominio que compramos debemos registrarlo, pero eso no bastará, sino que tenemos que ir a configurar nuestro dominio para que tolere nuestro servicio. Para ello una vez registrado el dominio procedemos a ir a la parte de configuración del dominio en MailGun y en la sección de verificación de dominio y DNS veremos la información necesaria para configurar nuevo dominio y activar el correo.

Una vez tenemos la información procedemos a visitar nuestro proveedor del dominio y vamos directamente a configurar ciertos parámetros internos del mismo. Para realizar esto seguimos las siguientes indicaciones:

  • Creando un campo TXT insertamos como valor "v=spf1 include:mailgun.org ~all"  (este caso debe ser directo al servicio, es decir sin ningún subdominio).
  • Creando otro campo TXT, pero con subdominio "smtp._domainkey" y le insertamos como valor la llave que MailGun nos provee para ese dominio.
  • Creamos un campo CNAME con subdominio "email" y le insertamos como valor "mailgun.org".
  • Creamos un campo MX con valor "mxa.mailgun.org"
  • Creamos un campo MX con valor "mxb.mailgun.org"


Que conste que estos valores son para el caso actual y pueden variar para el dominio en cuestión, por lo que es recomendable que usen los valores que el servicio les da.

Una vez terminado el proceso salvamos los cambios en el dominio y procedemos a esperar que MailGun nos confirme que hemos validado correctamente el dominio haciendo uso de la herramienta que el mismo servicio nos provee para refrescar y validar los datos del dominio. Dependiendo del proveedor del nombre de dominio podrían tardar unas horas o incluso días para ver estos cambios.

Una vez todo este correcto ya podemos proceder a escribir lo que enviará nuestro correo en la aplicación. Se podrán ver ejemplos en otros lenguajes visitando la página del servicio en cuestión (MailGun Doc).

Como MailGun tiene un servicio Restful podremos usar a Jersey en nuestro caso para enviar nuestros correos y solo necesitaremos los módulos:

  • jersey-client.jar (version ~ 1.17 - 1.18.1)
  • jersey-core.jar (version ~ 1.17 - 1.18.1)
  • jersey-multipart.jar (version ~ 1.17 - 1.18.1).

Una ves incluimos estas librerías en nuestro proyecto procedemos a crear una clase de ejemplo:

?

  
01
02
03
04
05
06
07
08
09
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
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.file.FileDataBodyPart;
import javax.ws.rs.core.MediaType;
import java.io.File;
import java.util.List;
public class Email {
    private Client client;
    private WebResource wrSender;
    private String apiKey;
    private String urlDomain;
    public Email(String apiKey, String urlDomain) {
        this.apiKey = apiKey;
        this.urlDomain = urlDomain;
        init();
    }
    private void init() {
        client = Client.create();
        client.addFilter(new HTTPBasicAuthFilter("api", apiKey));
        wrSender = client.resource(urlDomain + "/messages");
    }
    public String sendEmail(String from, String subject, String data, boolean asHTML, List<String> tos) {
        if (tos == null || tos.size() == 0) {
            return null;
        }
        MultivaluedMapImpl formData = new MultivaluedMapImpl();
        formData.add("from", from);
        for (String toSend : tos) {
            formData.add("to", toSend);
        }
        formData.add("subject", subject);
        if (asHTML) {
            formData.add("html", data);
        } else {
            formData.add("text", data);
        }
        return wrSender.type(MediaType.APPLICATION_FORM_URLENCODED)
                .post(ClientResponse.class, formData).getEntity(String.class);
    }
    public String sendEmail(String from, String subject, String data, boolean asHTML, List<String> tos, List<File> files) {
        if (tos == null || tos.size() == 0) {
            return null;
        }
        FormDataMultiPart formData = new FormDataMultiPart();
        formData.field("from", from);
        for (String toSend : tos) {
            formData.field("to", toSend);
        }
        formData.field("subject", subject);
        if (asHTML) {
            formData.field("html", data);
        } else {
            formData.field("text", data);
        }
        for (File file : files) {
            formData.bodyPart(new FileDataBodyPart("attachment", file, MediaType.TEXT_PLAIN_TYPE));
        }
        return wrSender.type(MediaType.MULTIPART_FORM_DATA_TYPE)
                .post(ClientResponse.class, formData).getEntity(String.class);
    }
}

  
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.file.FileDataBodyPart;

import javax.ws.rs.core.MediaType;
import java.io.File;
import java.util.List;

public class Email {

    private Client client;
    private WebResource wrSender;

    private String apiKey;
    private String urlDomain;

    public Email(String apiKey, String urlDomain) {
        this.apiKey = apiKey;
        this.urlDomain = urlDomain;
        init();
    }

    private void init() {
        client = Client.create();
        client.addFilter(new HTTPBasicAuthFilter("api", apiKey));
        wrSender = client.resource(urlDomain + "/messages");
    }

    public String sendEmail(String from, String subject, String data, boolean asHTML, List<String> tos) {
        if (tos == null || tos.size() == 0) {
            return null;
        }
        MultivaluedMapImpl formData = new MultivaluedMapImpl();
        formData.add("from", from);
        for (String toSend : tos) {
            formData.add("to", toSend);
        }
        formData.add("subject", subject);
        if (asHTML) {
            formData.add("html", data);
        } else {
            formData.add("text", data);
        }
        return wrSender.type(MediaType.APPLICATION_FORM_URLENCODED)
                .post(ClientResponse.class, formData).getEntity(String.class);
    }

    public String sendEmail(String from, String subject, String data, boolean asHTML, List<String> tos, List<File> files) {
        if (tos == null || tos.size() == 0) {
            return null;
        }
        FormDataMultiPart formData = new FormDataMultiPart();
        formData.field("from", from);
        for (String toSend : tos) {
            formData.field("to", toSend);
        }
        formData.field("subject", subject);
        if (asHTML) {
            formData.field("html", data);
        } else {
            formData.field("text", data);
        }
        for (File file : files) {
            formData.bodyPart(new FileDataBodyPart("attachment", file, MediaType.TEXT_PLAIN_TYPE));
        }
        return wrSender.type(MediaType.MULTIPART_FORM_DATA_TYPE)
                .post(ClientResponse.class, formData).getEntity(String.class);
    }
}

Por último para retocar nuestro contenido del correo y tener algo más empresarial podríamos usar un sistema de plantillas responsive como estos:

https://github.com/mailgun/transactional-email-templates
https://github.com/leemunroe/responsive-html-email-template
https://github.com/leemunroe/grunt-email-workflow

Masketfield en Vaadin

En algunos casos necesitamos filtrar la data que el cliente nos digita y esta sigue un patrón especifico, pero realizar esta tarea usando Listener del lado del servidor quizás no sea la forma más eficiente usando el Framework Vaadin.

Para mejorar y hacer más práctica esta tarea les traigo un plugin para realizar lo que se llama Mascara MasketField.

Con este plugin podrán tener máscaras rápidas en vaadin y todo del lado del cliente, sin la necesidad de fatigar el servidor con los listeners para atender dicha petición.

El plugin es Open y cualquier ayuda es bienvenida.