Wednesday, September 17, 2014

Embedding a XMPP server inside your JSF Web Application using Vysper, TomEE and PrimeFaces

I have an application that needs to notify the user when some job is done. It uses JSF and Primefaces, so this sort of notification could be implemented using atmosphere (aka Push).

But another funny approach is to use a XMPP server embedded in your java web application. Ok, ok, you don't have to embed it, you can just run an instance of a industrial-strenght XMPP server such as Openfire and Tigase. But hey, we're just playing a little, so I'll show you how to do it using Vysper, a little proof-of-concept developed using Apache Mina, that is simple enough for a few minutes play.

Before showing how to do it, it's nice to remember that threads and JEE applications usually don't mix, so we can play, but we must be aware of what are we doing.

First, you're going to need these JARs, most of them come from Vysper. Just a few huh?
  • aopalliance-1.0.jar
  • commons-codec-1.4.jar
  • commons-collections-3.1.jar
  • commons-io-1.4.jar
  • commons-lang-2.5.jar
  • commons-logging-1.1.jar
  • concurrent-1.3.4.jar
  • derby-10.2.1.6.jar
  • dnsjava-2.0.8.jar
  • ehcache-core-2.2.0.jar
  • fontbox-0.1.0.jar
  • jackrabbit-api-1.5.0.jar
  • jackrabbit-core-1.5.3.jar
  • jackrabbit-jcr-commons-1.5.3.jar
  • jackrabbit-spi-1.5.0.jar
  • jackrabbit-spi-commons-1.5.0.jar
  • jackrabbit-text-extractors-1.5.0.jar
  • jcl-over-slf4j-1.5.3.jar
  • jcr-1.0.jar
  • jempbox-0.2.0.jar
  • jetty-continuation-7.2.1.v20101111.jar
  • jetty-http-7.2.1.v20101111.jar
  • jetty-io-7.2.1.v20101111.jar
  • jetty-security-7.2.1.v20101111.jar
  • jetty-server-7.2.1.v20101111.jar
  • jetty-servlet-7.2.1.v20101111.jar
  • jetty-util-7.2.1.v20101111.jar
  • jetty-websocket-7.2.1.v20101111.jar
  • log4j-1.2.14.jar
  • lucene-core-2.3.2.jar
  • mina-core-2.0.2.jar
  • nbxml-0.7.jar
  • nekohtml-1.9.7.jar
  • pdfbox-0.7.3.jar
  • poi-3.0.2-FINAL.jar
  • poi-scratchpad-3.0.2-FINAL.jar
  • primefaces-4.0.jar
  • servlet-api-2.5.jar
  • slf4j-api-1.5.3.jar
  • slf4j-log4j12-1.5.3.jar
  • smack-3.1.0.jar
  • smackx-3.1.0.jar
  • spec-compliance-0.7.jar
  • spring-aop-3.0.5.RELEASE.jar
  • spring-asm-3.0.5.RELEASE.jar
  • spring-beans-3.0.5.RELEASE.jar
  • spring-context-3.0.5.RELEASE.jar
  • spring-core-3.0.5.RELEASE.jar
  • spring-expression-3.0.5.RELEASE.jar
  • vysper-core-0.7.jar
  • vysper-websockets-0.7.jar
  • xep0045-muc-0.7.jar
  • xep0060-pubsub-0.7.jar
  • xep0124-xep0206-bosh-0.7.jar
  • xercesImpl-2.8.1.jar
  • xml-apis-1.3.03.jar
Now, copy from Vysper the bogus certificate, so your XMPP server can "work" under a "secure" channel. It's called bogus_mina_tls.cert.

My xhtml looks like this

<?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:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui">
<h:head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Messaging Prototype</title>
    <link rel="icon" type="image/png" href="favicon.ico"></link>   
</h:head>
<h:body>   
    <h:outputStylesheet library="css" name="style.css"  />

    <p:ajaxStatus style="width:16px;height:16px;" id="ajaxStatusPanel"> 
        <f:facet name="start"> 
            <h:graphicImage value="./ajaxloading.gif" /> 
        </f:facet> 
         
        <f:facet name="complete"> 
            <h:outputText value="" /> 
        </f:facet> 
    </p:ajaxStatus> 

    <h:form>   
        <p:messages id="messages" showDetail="true" autoUpdate="true" closable="true" />
        <p:spacer height="10" />
        <p:panel>
            <h:panelGrid columns="2">
                <p:commandButton value="Enter" action="#{messagingMB.sendMessage}" />
            </h:panelGrid>
        </p:panel>
        <p:spacer height="10" />
    </h:form>
</h:body>
</html>


Pretty simple huh? The Managed Bean is also easy.



import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

import org.jivesoftware.smack.XMPPException;

@ManagedBean
@ViewScoped
public class MessagingMB implements Serializable {

    private static final long    serialVersionUID    = -9092497421080796430L;
   
    @EJB
    private JSFUtilEJB jsfUtilEJB;
   
    @PostConstruct
    public void init() {
    }

    public void sendMessage() {
        try {
            new BasicClient().test();
        } catch (XMPPException e) {
            jsfUtilEJB.addErrorMessage(e,"Could not send");
        }
    }
}


and of course, the EJBs



import javax.ejb.Stateless;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.context.FacesContext;

@Stateless
public class JSFUtilEJB {

    @SuppressWarnings("unchecked")
    public <T> T findBean(String beanName) {
        FacesContext context = FacesContext.getCurrentInstance();
        return (T) context.getApplication().evaluateExpressionGet(context, "#{" + beanName + "}", Object.class);
    }

    public long addErrorMessage(String msg) {
        return addMessage(null,FacesMessage.SEVERITY_ERROR,msg);
    }
    public long addErrorMessage(Exception e,String summary){
        return addMessage(e,FacesMessage.SEVERITY_ERROR,summary);
    }
    public long addFatalErrorMessage(Exception e,String summary){
        return addMessage(e,FacesMessage.SEVERITY_FATAL,summary);
    }
    public long addInfoMessage(String summary){
        return addMessage(null,FacesMessage.SEVERITY_INFO,summary);
    }
    public long addWarnMessage(Exception e,String summary){
        return addMessage(e,FacesMessage.SEVERITY_WARN,summary);
    }

    public long addErrorMessage(Exception e) {
        return addMessage(e,FacesMessage.SEVERITY_ERROR,e.getMessage(),e.getClass().getSimpleName());
    }

    private long addMessage(Exception e,Severity severity, String summary) {
        FacesContext context = FacesContext.getCurrentInstance();
        String clientId = null;
       
        long id = -1;
       
        if (e != null){
            id = printStackTrace(e);
            FacesMessage facesMessage = null;
            if (e.getCause() instanceof org.apache.openjpa.persistence.EntityExistsException){
                facesMessage = new FacesMessage(severity,"[Error: #"+id+"] "+summary,"You are trying are to add a new object that already exists or your're trying to violate a unique constraint)" );   
            }else{
                facesMessage = new FacesMessage(severity,"[Error: #"+id+"] "+summary,e.getMessage() );
            }
            context.addMessage(clientId , facesMessage );
        }else{
            FacesMessage facesMessage = new FacesMessage(severity,summary," ");
            context.addMessage(clientId , facesMessage );
        }

        return id;
    }

    private long addMessage(Exception e,Severity severity, String summary, String detail) {
        FacesContext context = FacesContext.getCurrentInstance();
        String clientId = null;
       
        long id = -1;
        if (e != null){
            id = printStackTrace(e);       
            FacesMessage facesMessage = new FacesMessage(severity,"["+id+"] "+summary,detail );
            context.addMessage(clientId , facesMessage );
        }else{
            FacesMessage facesMessage = new FacesMessage(severity,summary,detail );
            context.addMessage(clientId , facesMessage );
        }
       
        return id;
    }

    public long printStackTrace(Exception e){
        long uniqueId = System.currentTimeMillis();
        return uniqueId;
    }

}


and



import java.io.File;
import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;

import org.apache.vysper.mina.TCPEndpoint;
import org.apache.vysper.storage.StorageProviderRegistry;
import org.apache.vysper.storage.inmemory.MemoryStorageProviderRegistry;
import org.apache.vysper.xmpp.addressing.EntityImpl;
import org.apache.vysper.xmpp.authorization.AccountManagement;
import org.apache.vysper.xmpp.server.XMPPServer;

@Startup
@Singleton
public class XmppEJB implements Serializable {
   
    /**
     * <br>06/09/2014
     */
    private static final long serialVersionUID = 1L;
    private boolean started;

    @PostConstruct
    public void init() {
        try {
            // choose the storage you want to use
    //        StorageProviderRegistry providerRegistry = new JcrStorageProviderRegistry();
            StorageProviderRegistry providerRegistry = new MemoryStorageProviderRegistry();
   
            final AccountManagement accountManagement = (AccountManagement) providerRegistry.retrieve(AccountManagement.class);
   
            if(!accountManagement.verifyAccountExists(EntityImpl.parse("user1@vysper.org"))) {
                accountManagement.addUser(EntityImpl.parse("user1@vysper.org"), "password");
            }
            if(!accountManagement.verifyAccountExists(EntityImpl.parse("user2@vysper.org"))) {
                accountManagement.addUser(EntityImpl.parse("user2@vysper.org"), "password");
            }
           
            XMPPServer server = new XMPPServer("vysper.org");
            server.addEndpoint(new TCPEndpoint());
            server.setStorageProviderRegistry(providerRegistry);
            server.setTLSCertificateInfo(new File("/path/to/bogus_mina_tls.cert"), "boguspw");
           
            server.start();
            System.out.println("server is running...");
        } catch (Exception e) {
            e.printStackTrace();
        }
       
        started = true;
    }

    public boolean isStarted() {
        return this.started;
    }
   
   
   
}


and the basic client, which comes from Vysper.

import java.util.Date;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.packet.Time;
import org.jivesoftware.smackx.packet.Version;

public class BasicClient {

    static class IQListener implements PacketListener {

        public void processPacket(Packet packet) {
            IQ iq = (IQ) packet;
            String iqString = iq.toString();
            System.out.println("T" + System.currentTimeMillis() + " IQ: "
                    + iqString + ": " + iq.toXML());
        }
    }

    static class PresenceListener implements PacketListener {

        public void processPacket(Packet packet) {
            Presence presence = (Presence) packet;
            String iqString = presence.toString();
            final PacketExtension extension = presence
                    .getExtension("http://jabber.org/protocol/caps");
            if (extension != null)
                System.out.println("T" + System.currentTimeMillis() + " Pres: "
                        + iqString + ": " + presence.toXML());
        }
    }

    public void test() throws XMPPException {

        String me = "user2@vysper.org";
        String to = "user1@vysper.org";

        try {
            ConnectionConfiguration connectionConfiguration = new ConnectionConfiguration(
                    "localhost");
            connectionConfiguration.setCompressionEnabled(false);
            connectionConfiguration.setSelfSignedCertificateEnabled(true);
            connectionConfiguration.setExpiredCertificatesCheckEnabled(false);
//            connectionConfiguration.setDebuggerEnabled(true);
            connectionConfiguration
                    .setSecurityMode(ConnectionConfiguration.SecurityMode.required);
//            XMPPConnection.DEBUG_ENABLED = true;
            XMPPConnection connection = new XMPPConnection(
                    connectionConfiguration);
            connection.connect();

            SASLAuthentication saslAuthentication = connection
                    .getSASLAuthentication();
            saslAuthentication.authenticate(me, "password", "test");
            connection.login(me, "pqssword");

            connection.getRoster().setSubscriptionMode(
                    Roster.SubscriptionMode.accept_all);

            connection.addPacketListener(new IQListener(), new PacketFilter() {
                public boolean accept(Packet packet) {
                    return packet instanceof IQ;
                }
            });

            connection.addPacketListener(new PresenceListener(),
                    new PacketFilter() {
                        public boolean accept(Packet packet) {
                            return packet instanceof Presence;
                        }
                    });

            Chat chat = null;
            if (to != null) {
                Presence presence = new Presence(Presence.Type.subscribe);
                presence.setFrom(connection.getUser());
                String toEntity = to;
                presence.setTo(toEntity);
                connection.sendPacket(presence);

                chat = connection.getChatManager().createChat(toEntity,
                        new MessageListener() {
                            public void processMessage(Chat inchat,
                                    Message message) {
                                System.out.println("log received message: "
                                        + message.getBody());
                            }
                        });
            }

            connection.sendPacket(new Presence(Presence.Type.available,
                    "pommes", 1, Presence.Mode.available));

            Thread.sleep(1000);

            // query server version
            sendIQGetWithTimestamp(connection, new Version());

            // query server time
            sendIQGetWithTimestamp(connection, new Time());

            chat.sendMessage("Hello " + to + " at " + new Date());

            connection.disconnect();
        } catch (Throwable e) {
            e.printStackTrace(); // To change body of catch statement use File |
                                    // Settings | File Templates.
        }
        System.out.println("bye");
    }

    private static void sendIQGetWithTimestamp(XMPPConnection connection, IQ iq) {
        iq.setType(IQ.Type.GET);
        connection.sendPacket(iq);
        System.out.println("T" + System.currentTimeMillis()
                + " IQ request sent");
    }
}


We're almost set. Of course, now we need a XMPP client, such as Pidgin

First, let me just say thanks to this blog, because I don't know why, Vysper site has little to no information about how to configure Pidgin, so this blog post was really useful.

Let me show you how my pidgin user looks like








I know, it's in Portuguese.


And that's it. We're all set. Start your JSF web application and play.

Notice that the communication is bidirectional, so you can just use your XMPP client to send commands to the server. To do that, you just have to change this listener

                chat = connection.getChatManager().createChat(toEntity,
                        new MessageListener() {
                            public void processMessage(Chat inchat,
                                    Message message) {
                                System.out.println("log received message: "
                                        + message.getBody());
                            }
                        });


I wonder if we could just create a DSL to process some commands and if we could find some autocomplete pidgin plugin to write commands using this DSL. Suggestions are welcome. ;-)

ps. The EJB does not shutdown the server gracefully. But I bet there's some EJB annotation to do that in an event of a server shutdown.

Saturday, January 18, 2014

How to send an array of objects using Soap UI


Let me show you an example and maybe it can help you.

    package mypackage;

    import java.io.Serializable;
    import java.util.Date;

    public class Thing implements Serializable{

        private static final long serialVersionUID = 4205832525113691806L;
        private String name;
        private Date date;
        private Long longg;

        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Date getDate() {
            return date;
        }
        public void setDate(Date date) {
            this.date = date;
        }
        public Long getLongg() {
            return longg;
        }
        public void setLongg(Long longg) {
            this.longg = longg;
        }
        @Override
        public String toString() {
            return "Thing [name=" + name + ", date=" + date + ", longg=" + longg + "]";
        }
    }

and the web service

    package mypackage;

    import java.util.Arrays;

    import javax.ejb.Stateless;
    import javax.jws.WebService;

    @WebService
    @Stateless
    public class WS {   
        public void doSomething(Thing[] things){
            System.out.println(Arrays.asList(things));
        }
    }

then if you use soapUI to generate the request for you, you'll get something like this



and the result will be (in your server logs)

    [Thing [name=aeoliam venit, date=Sun Sep 28 22:49:45 BRT 2008, longg=10]]

but, of course, you want to send an array of these things, so...





and voila, the result will be

    [Thing [name=aeoliam venit, date=Sun Sep 28 22:49:45 BRT 2008, longg=10], Thing [name=aeoliam venit, date=Sun Sep 28 22:49:45 BRT 2008, longg=10]]

gotcha? :-)

it´s incredible that we just can't find anywhere this answer.

Tuesday, January 14, 2014

Primefaces DataTable Lazy Loading with pagination, filtering and sorting using JPA Criteria, @ViewScoped

Primefaces datatable lazy pagination works, but I was really frustrated after searching for a full example using Criteria in the web. So I've mixed solutions from

  • http://stackoverflow.com/questions/13972193/how-to-query-data-for-primefaces-datatable-with-lazy-loading-and-pagination
  • http://www.primefaces.org/showcase/ui/datatableLazy.jsf
  • http://www.javacodegeeks.com/2012/04/lazy-jsf-primefaces-datatable.html

To put all together in a complete example using

  • Primefaces 4.0
  • TomEE 1.6.0+ 
  • Criteria JPA
With


  • Filtering
  • Sorting
  • Pagination
  • Using @ViewScoped Managed Beans
So let's go

xhtml snippet

 <?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:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui">
<h:body>
    <h:form id="am">

            <p:dataTable
                var="element"
                value="#{inventoryManagerMB.model}"
                lazy="true"
                paginator="true"
                rows="10"
                paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}"
                rowsPerPageTemplate="10,50,100"
                id="sites">
 
            <p:column sortBy="id" filterBy="id" filterMatchMode="contains" headerText="ID">                
                <h:outputText value="#{element.id}" />
            </p:column>
            <p:column sortBy="name" filterBy="name" filterMatchMode="contains" headerText="Name">                
                <h:outputText value="#{element.name}" />
            </p:column>
            <p:column sortBy="siteType.name" filterBy="siteType.name" filterMatchMode="contains" headerText="Type">                
                <h:outputText value="#{element.siteType.name}" />
            </p:column>
            <p:column sortBy="ip" filterBy="ip" filterMatchMode="contains" headerText="IP">                
                <h:outputText value="#{element.ip} " />
            </p:column>
            <p:column sortBy="description" filterBy="description" filterMatchMode="contains" headerText="Description">                
                <h:outputText value="#{element.description}" />
            </p:column>
            </p:dataTable>
    </h:form>
</h:body>
</html>



 ManagedBean

@ManagedBean
@ViewScoped
public class InventoryManagerMB implements Serializable {

    private static final long serialVersionUID = -1201944101993687165L;

    @EJB
    private InventoryManagerEJB inventoryManagerEJB;
   
    private LazyDataModel<Site> model;
 



     @PostConstruct
    public void init() {
        try {
            this.model = new LazyDataModel<Site>(){
                private static final long    serialVersionUID    = 1L;
                @Override
                public List<Site> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, String> filters) {
                    List<Site> result = inventoryManagerEJB.getResultList(first, pageSize, sortField, sortOrder, filters);
                    model.setRowCount(inventoryManagerEJB.count(sortField, sortOrder, filters));
                    return result;
                }
            };
}


    public LazyDataModel<Site> getModel() {
        return model;
    }

    public void setModel(LazyDataModel<Site> model) {
        this.model = model;
    }
 

(...)

EJB

@Stateless
public class InventoryManagerEJB {
   
    @Inject
    private BaseService baseService;


    public List<Site> getResultList(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, String> filters) {
        List<Site> all = new ArrayList<Site>();
        all.addAll(this.baseService.getSiteDAO().getAll(first,pageSize,sortField,sortOrder,filters));
        return all;
    }

    public int count(String sortField, SortOrder sortOrder, Map<String, String> filters) {
        return this.baseService.getSiteDAO().getAll(-1,-1,null,null,filters).size();
    }
}


BaseService

@ApplicationScoped
public class BaseService implements Serializable{
   
    private static Logger log = Logger.getLogger(BaseService.class);
   
    /*
     * persistence
     */
   
    private static final long serialVersionUID = 588696475267901772L;

    @PersistenceContext

    private EntityManager entityManager;

    private SiteDAO siteDAO;


    @PostConstruct
    public void init() {
            siteDAO = new SiteDAO(entityManager);
    }



    public SiteDAO getSiteDAO() {
        return siteDAO;
    }


SiteDAO

public class SiteDAO extends GenericDAO<Site>{

    public SiteDAO(EntityManager entityManager) {
        super(entityManager);
    }


    public Collection<Site> getAll(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, String> filters) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Site> q = cb.createQuery(Site.class);
        Root<Site> site = q.from(Site.class);
        Join<Site,SiteType> siteType = site.join(Site_.siteType);
        q.select(site);


        Path<?> path = getPath(sortField, site, siteType);
        if (sortOrder == null){
            //just don't sort
        }else if (sortOrder.equals(SortOrder.ASCENDING)){
            q.orderBy(cb.asc(path));
        }else if (sortOrder.equals(SortOrder.DESCENDING)){
            q.orderBy(cb.asc(path));
        }else if (sortOrder.equals(SortOrder.UNSORTED)){
            //just don't sort
        }else{
            //just don't sort
        }
       
        //filter
        Predicate filterCondition = cb.conjunction();
        for (Map.Entry<String, String> filter : filters.entrySet()) {
            if (!filter.getValue().equals("")) {
                //try as string using like
                Path<String> pathFilter = getStringPath(filter.getKey(), site, siteType);
                if (pathFilter != null){
                    filterCondition = cb.and(filterCondition, cb.like(pathFilter, "%"+filter.getValue()+"%"));
                }else{
                    //try as non-string using equal
                    Path<?> pathFilterNonString = getPath(filter.getKey(), site, siteType);
                    filterCondition = cb.and(filterCondition, cb.equal(pathFilterNonString, filter.getValue()));
                }
            }
        }
        q.where(filterCondition);
       
        //pagination
        TypedQuery<Site> tq = entityManager.createQuery(q);
        if (pageSize >= 0){
            tq.setMaxResults(pageSize);
        }
        if (first >= 0){
            tq.setFirstResult(first);
        }
        return tq.getResultList();
    }

    private Path<?> getPath(String field, Root<Site> site, Join<Site, SiteType> siteType) {
        //sort
        Path<?> path = null;
        if (field == null){
            path = site.get(Site_.name);
        }else{
            switch(field){
                case "id":
                    path = site.get(Site_.id);
                    break;
                case "name":
                    path = site.get(Site_.name);
                    break;
                case "siteType.name":
                    path = siteType.get(SiteType_.name);
                    break;
                case "ip":
                    path = site.get(Site_.ip);
                    break;
                case "description":
                    path = site.get(Site_.description);
                    break;
            }
        }
        return path;
    }

    private Path<String> getStringPath(String field, Root<Site> site, Join<Site, SiteType> siteType) {
        //sort
        Path<String> path = null;
        if (field == null){
            path = site.get(Site_.name);
        }else{
            switch(field){
                case "id":
                    path = null;
                    break;
                case "name":
                    path = site.get(Site_.name);
                    break;
                case "siteType.name":
                    path = siteType.get(SiteType_.name);
                    break;
                case "ip":
                    path = site.get(Site_.ip);
                    break;
                case "description":
                    path = site.get(Site_.description);
                    break;
            }
        }
        return path;
    }
}


Entity

@Entity
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"name"}))
public class Site implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 8008732613898597654L;
   
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
   
    @Nameable
    @Column(nullable=false)
    private String name;
   
    private String ip;   
   
    private String description;
   
    @ManyToOne
    @JoinColumn(name="siteTypeId")
    private SiteType siteType;


(...)
}


That's it. Enjoy.

Wednesday, January 8, 2014

Playing with ANTLR4, Primefaces extensions for Code Mirror and web-based DSLs


DSLs are cool things, but it wasn't clear to me what they were good for.

Then I realized they are good for:

  • get rid of complex UIs
 which means

  • faster way to do things

That's it.  I've came to this conclusion when I read this blog.

If your user is techie and are not afraid of a SQL-like syntax way of doing things, DSLs are great specially if

  • you have syntax highlight
  • you have code completion
Otherwise, DSLs kinda sucks.

So, I had to present some proof of concept to a client. He has fuzzy requirements and it's not easy to extract exactly what his team needs (they need a lot of things and he's very busy), so a DSL can help a lot in this process because people are forced to think exactly what they need when they face a grammar (even a small one).

So I went with these technologies:

Unfortunately, I could not reuse the grammar in both tools. Actually, I could not find any solution that could do that. At least for a web-based JSF solution. And there was no time to learn. So I had to hack a little.

First, we need the grammar. ANTLR4 is way better than ANTLR3 because now the wiring code is done via visitors and listeners. No more java code inside the grammar. That's great and much easier to work with.

So you can have a grammar like this

grammar Grammar;
options
{
    language = Java;
}
@lexer::header {
  package parsers;
}

@parser::header {
  package parsers;
}
eval     :     expr EOF;
expr    :    'JOB' (jobName)? type 'TARGET' targetList ('START' startExpr)?
startExpr   
    :    'AT' cronTerm
    |    'AFTER' timeAmount timeUnits;
timeAmount: INT;   
jobName: STRING;   
targetList: STRING (',' STRING)*;   
type    :    deleteUser
    |    createUser;

deleteUser: opDelete userName;
createUser: opCreate userName;
opDelete: 'DELETE';
opCreate: 'CREATE';
userName: STRING;
   
cronTerm: '!'? (INT | '-' | '/' | '*' | '>' | '<')+;

timeUnits
    :    'MINUTES'
    |    'HOURS'
    |    'DAYS'
    |    'WEEKS'
    |    'MONTHS';


WS  :   [ \t\r\n]+ -> skip;

STRING
    :  '"' ( ESC_SEQ | ~('\\'|'"') )* '"'
    ;

fragment
HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;

fragment
ESC_SEQ
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    |   UNICODE_ESC
    |   OCTAL_ESC
    ;

fragment
OCTAL_ESC
    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7')
    ;

fragment
UNICODE_ESC
    :   '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
    ;

ID  :    ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
    ;

INT :    '0'..'9'+
    ;



To compile your grammar, try

    public static void main(String[] args) {
        String[] arg0 = {"-visitor","/pathto/Grammar.g4"};
        org.antlr.v4.Tool.main(arg0);
    }


Then ANTLR will generate the classes for you.

In our case, we want to visit the parse tree and retrieve the values we want. We do this extending the generated abstract class.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.antlr.v4.runtime.tree.ErrorNode;

import bsh.EvalError;
import bsh.Interpreter;

public class MyLoader2 extends GrammarBaseVisitor<Void> {

    private String jobName,cronTerm,timeUnits,userName,jobType;
    private List<String> targetList;
    private boolean now,errorFound;
    private int timeAmount;   
    private Interpreter bsh = new Interpreter();
   
    private String eval(String s) throws EvaluationException{
        try {
            if (!s.startsWith("\"")){
                return s;
            }
            bsh.eval("String s="+s);
            return (String)bsh.eval("s");
        } catch (EvalError e) {
            throw new EvaluationException(s);
        }
    }
   
    @Override
    public Void visitTimeAmount(TimeAmountContext ctx) {
        try{
            this.timeAmount = Integer.parseInt(ctx.getText());
        }catch(java.lang.NumberFormatException nfe){
            throw new InvalidTimeAmountException(ctx.getText());
        }

        return super.visitTimeAmount(ctx);
    }
    @Override
    public Void visitUserName(UserNameContext ctx) {
        this.userName = eval(ctx.getText());
        return super.visitUserName(ctx);
    }
    @Override
    public Void visitCronTerm(CronTermContext ctx) {
        this.cronTerm = eval(ctx.getText());
        return super.visitCronTerm(ctx);
    }
    @Override
    public Void visitTimeUnits(TimeUnitsContext ctx) {
        this.timeUnits = ctx.getText();
        return super.visitTimeUnits(ctx);
    }
    @Override
    public Void visitTargetList(TargetListContext ctx) {
        this.targetList = toStringList(ctx.getText());
        return super.visitTargetList(ctx);
    }
    @Override
    public Void visitJobName(JobNameContext ctx) {
        this.jobName = eval(ctx.getText());
        return super.visitJobName(ctx);
    }
    @Override
    public Void visitOpCreate(OpCreateContext ctx) {
        this.jobType = ctx.getText();
        return super.visitOpCreate(ctx);
    }
    @Override
    public Void visitOpDelete(OpDeleteContext ctx) {
        this.jobType = ctx.getText();
        return super.visitOpDelete(ctx);
    }
    private List<String> toStringList(String text) {
        List<String> l = new ArrayList<String>();
        StringTokenizer st = new StringTokenizer(text," ,");
        while(st.hasMoreElements()){
            l.add(eval(st.nextToken()));
        }
        return l;
    }
   
    private Map<String, String> toMapList(String text) throws InvalidItemsException, InvalidKeyvalException {
        Map<String, String> m = new HashMap<String, String>();
        if (text == null || text.trim().length() == 0){
            return m;
        }
        String[] items = text.split(",");
        if (items.length == 0){
            throw new InvalidItemsException();
        }
        for(String item:items){
            String[] keyval = item.split("=");
            if (keyval.length == 2){
                m.put(keyval[0], keyval[1]);
            }else{
                throw new InvalidKeyvalException(keyval.length);
            }
        }
        return m;
    }
    public String getJobName() {
        return jobName;
    }
    public String getCronTerm() {
        return cronTerm;
    }
    public String getTimeUnits() {
        return timeUnits;
    }
    public String getUserName() {
        return userName;
    }
    public String getJobType() {
        return jobType;
    }
    public List<String> getTargetList() {
        return targetList;
    }
    public boolean isNow() {
        return now;
    }
    public int getTimeAmount() {
        return timeAmount;
    }
    @Override
    public Void visitOpNow(OpNowContext ctx) {
        this.now = ctx.getText().equals("NOW");
        return super.visitOpNow(ctx);
    }
    public boolean isErrorFound() {
        return errorFound;
    }
    @Override
    public Void visitErrorNode(ErrorNode node) {
        this.errorFound = true;
        return super.visitErrorNode(node);
    }
}

Notice the beanshell interpreter being used to evaluate a string like "xyz" to be xyz. This is specially useful for strings that contains escaped quotes and characteres inside.

So, you have your grammar and your visitor / loader bean, then we can test it:

private static MyLoader getLoader(String str){
    ANTLRInputStream input = new ANTLRInputStream(str);
    GrammarLexer lexer = new GrammarLexer(input);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    GrammarParser parser = new GrammarParser(tokens);
    ParseTree tree = parser.eval();
    MyLoader loader = new MyLoader();
    loader.visit(tree);
    return loader;
}

public static void main(String[] args){
    MyLoader loader = getLoader("JOB \"jobName\" CREATE \"myuser\" TARGET \"site1\",\"site2\" START AFTER 1 DAY");
    System.out.println(loader.getJobName());
    System.out.println(loader.getJobType());
}


Great. Now it's the hack. Code Mirror has support for custom grammars, but it's not present in the JSF Primefaces extension. So I've opened the resources-codemirror-1.2.0.jar, opened the /META-INF/resources/primefaces-extensions/codemirror/mode/modes.js file, formatted it (so I could read it) and I've just chosen the simplest language to be my new custom sintax highlighter :-)

I renamed this one

(...)
}, "xml"), CodeMirror.defineMIME("text/x-markdown", "markdown"), CodeMirror.defineMode("mylanguage", function (e) {
(...)
    var t = e.indentUnit,
        n, i = r(["site", "type", "targetList"]),
        s = r(["AT","AFTER","CREATE","MINUTES","HOURS","TARGET","MONTHS","JOB","DAYS","DELETE","START","WEEKS" ]),
(...)
}), CodeMirror.defineMIME("text/x-mylanguage", "mylanguage"), CodeMirror.defineMode("ntriples", function () {(...)


those guys in uppercase in the "s = r" are tokens that will be highlighted, while those one in the "i = r" are tokens that won't be highlighted. Why do we want both? Because the second type is the "placeholder", I mean, we'll use them for the autocomplete stuff.

well, then your JSF xhtml page will look like this

<?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:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    xmlns:pe="http://primefaces.org/ui/extensions">

<h:body>
<h:form id="form">
<pe:codeMirror
    id="codeMirror"
    style="width:600px;" 
    mode="myLanguage" 
    widgetVar="myCodeMirror" 
    theme="eclipse"
    value="#{myMB.script}"
    lineNumbers="true"             
    completeMethod="#{myMB.complete}" 
    extraKeys="{ 'Ctrl-Space': function(cm) { PF('myCodeMirror').complete(); }}"/> 
<p:commandButton value="Verify" action="#{myMB.verify}" />

(...)

now, we need the autocomplete stuff. It's the boring part. You have to do most of the completion stuff manually, because there's no context information (remember, I don't have time to learn...), so the quick and dirty way to do it is like this

in myMB

    public List<String> complete(final CompleteEvent event) {
        try {
            return this.myEJB.complete(event.getToken());
        } catch (Exception e) {
            jsfUtilEJB.addErrorMessage(e,"Could not complete");
            return null;
        }
    } 


in myEJB

private static final String    SITE    = "site_";
public List<String> complete(String token) throws Exception {
    if (token == null || token.trim().length() == 0){
        return null;
    }else{
        List<String> suggestions = new ArrayList<String>();
       
        switch(token){
            //first search variables
            case "targetlist":
                for(String v:TARGETS){
                    suggestions.add(v);   
                }
                break;
            case "site":
                List<Site> allSites = this.baseService.getSiteDAO().getAll();
                for(Site s:allSites){
                    suggestions.add("\""+SITE+s.getName()+"\"");
                }
                break;
            case "type":
                suggestions.add("DELETE \"userName\"");
                suggestions.add("CREATE \"userName\"");
                break;
            case "AT":
                suggestions.add("AT \"cronExpression\"");
                suggestions.add("AT \"0 * * * * * * *\"");
                break;
            case "AFTER":
                for(int a:AMOUNTS){
                    for(String u:UNITS){
                        if (a == 1){
                            suggestions.add("AFTER"+" "+a+" "+u);
                        }else{
                            suggestions.add("AFTER"+" "+a+" "+u+"S");
                        }
                    }
                }
                break;
            case "TARGET":
                for(String v:TARGETS){
                    suggestions.add("TARGET "+v+"");   
                }
                break;
            case "JOB":
                suggestions.add("JOB \"jobName\" \ntype \nTARGET targetlist \nSTART");
                break;
            case "START":
                suggestions.add("START AT \"cronExpression\"");
                suggestions.add("START AT \"0 * * * * * * *\"");
                for(int a:AMOUNTS){
                    for(String u:UNITS){
                        if (a == 1){
                            suggestions.add("START AFTER"+" "+a+" "+u);
                        }else{
                            suggestions.add("START AFTER"+" "+a+" "+u+"S");
                        }
                    }
                }
                suggestions.add("START NOW");
                break;
            case "DELETE":
                suggestions.add("DELETE \"userName\"");
                break;
            case "CREATE":
                suggestions.add("CREATE \"userName\"");
                break;

            default:
                if (token.startsWith(SITE)){
                    List<Site> matchedSites = this.baseService.getSiteDAO().getByPattern(token.substring(SITE.length())+"*");
                    for(Site s:matchedSites){
                        suggestions.add("\""+SITE+s.getName()+"\"");
                    }
                }else{
                    //then search substrings
                    for(String kw:KEYWORDS){
                        if (kw.toLowerCase().startsWith(token.toLowerCase())){
                            suggestions.add(kw);
                        }
                    }
                }
        }//end switch
       
        //remove dups and sort
        Set<String> ts = new TreeSet<String>(suggestions);           
        return new ArrayList<String>(ts);
    }
}

private static final int[] AMOUNTS = {1,5,10};
private static final String[] UNITS = {"MINUTE","HOUR","DAY","WEEK","MONTH"};
private static final String[] TARGETS = {"site"};

/*
 * KEYWORDS are basic suggestions
 */
private static final String[] KEYWORDS = {"AT","AFTER","CREATE","MINUTES","HOURS","TARGET","MONTHS","JOB","DAYS","DELETE","START","WEEKS"};


so the autocomplete stuff for keywords will just show you fields and more keywords, while the "placeholders" (remember those lowercase keywords in the codemirror javascript from the jar?) are completed with dynamic values retrieved from the DB, for actual values. Also, you can use partial strings to retrieve those ones that start with a substring, like this:



of course, pattern-like searches in JPA can be performed like this

public abstract class GenericDAO<E> {
   
    protected EntityManager entityManager;
   
    private Class<E> clazz;
    private EntityType<E> pClass;


    @SuppressWarnings("unchecked")
    public GenericDAO(EntityManager entityManager) {
        this.entityManager = entityManager;
    ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
    this.clazz = (Class<E>) genericSuperclass.getActualTypeArguments()[0];

    EntityManagerFactory emf = this.entityManager.getEntityManagerFactory();
    Metamodel metamodel = emf.getMetamodel();
    this.pClass = metamodel.entity(clazz);
    }

    public List<E> getByPattern(String pattern) {
        pattern = pattern.replace("?", "_").replace("*", "%");
   
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<E> q = cb.createQuery(clazz);
        Root<E> entity = q.from(clazz);
        SingularAttribute<E, String> singularAttribute = (SingularAttribute<E, String>) pClass.getDeclaredSingularAttribute(getNameableField(clazz));
        Path<String> path = entity.get(singularAttribute);
        q.where(cb.like(path, pattern));
        q.select(entity);
   
        TypedQuery<E> tq = entityManager.createQuery(q);
        List<E> all = tq.getResultList();
   
        return all;
    }

    private String getNameableField(Class<E> clazz) {
        for(Field f : clazz.getDeclaredFields()) {
            for(Annotation a : f.getAnnotations()) {
                if(a.annotationType() == Nameable.class) {
                    return f.getName();
                }
            }
        }
        return null;
    }
(...)


where Nameable is an annotation for your entity classes

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Nameable {
}


Use it to annotate a single column from your entity class, a String one. Like this:

@Entity
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"name"}))
public class Site implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 8008732613898597654L;
   
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
   
    @Nameable
    @Column(nullable=false)
    private String name;
(...)


And the "verify" button, of course, just get your script and pushes into the loader.










Friday, December 27, 2013

Java pattern matching using GLOB patterns (* and ?)

Java has partial glob support since java 7 filePath, but truly glob support I could only find in the good, old, retired, jakarta-oro lib.

Here goes a simple example of how to use it

for(String regex:regexes){
  
    //it's the unix GLOB regex. Java itself does not support it
    //but the good old jakarta oro does
    GlobCompiler compiler=new GlobCompiler();
    Perl5Matcher matcher=new Perl5Matcher();
  
    //there are some interesting options you may want to try
    //org.apache.oro.text.regex.Pattern pat = compiler.compile(regex,GlobCompiler.QUESTION_MATCHES_ZERO_OR_ONE_MASK );
    org.apache.oro.text.regex.Pattern pat = compiler.compile(regex.trim());
    if (site != null && matcher.matches(site, pat)){
        passwordPattern = pattern;
        break;
    }
}

Tuesday, December 17, 2013

Primefaces: capturing the target list item clicked

First of all, kudos to Hatem Alimam for his answer, who solved this problem.

Here's the problem.

I have a @ViewScoped managed bean with a p:pickList filtered by a p:selectMany component using a p:ajax update event.

Like this

<p:outputLabel for="siteFilter" value="Site Filter:" style="width:100px;"/>
<p:selectOneMenu converter="#{siteTypeConverter}" id="siteFilter" value="#{myMB.selectedSiteType}" style="width:200px;">              
   <f:selectItem itemLabel="Select Site Type" itemValue="#{null}" />
   <f:selectItems value="#{myMB.siteTypeFilterList}" var="st" itemLabel="#{st.name}" itemValue="#{st}" />
   <p:ajax update="sites" listener="#{myMB.handleSiteTypeFilterChange}" oncomplete="rebindClicks()"/>
</p:selectOneMenu>

<p:outputLabel for="sites" value="Sites:" style="width:100px;"/>
<p:pickList
   widgetVar="xyzabc"
   id="sites"
   value="#{myMB.sites}"
   var="site"                 
   converter="#{siteConverter}"
   itemLabel="#{site.siteType.name}.#{site.name}"
   itemValue="#{site}" /> 

The idea here is to trigger a new event when the user clicks on some pickList target list item.

So, as suggested by @Hatem, we have to bind the click event to each one of the target elements. Like this

(...)
   <p:remoteCommand name="updateCommand" action="#{myMB.updateAccounts}" update="accounts"/>
</h:form>
<h:outputScript library="js" name="pickListHack.js" />   
(...)

Notice that I had to add it AFTER the form.

and pickListHack.js is located at WebContent/resources/js/pickListHack.js

function rebindClicks() {
    xyzabc.jq.on('click','.ui-picklist-target li',function(){
        var item = $(this).attr('data-item-value');
        alert(item);
        updateCommand([{name:'param', value:item}]);
   });
};

$(document).ready(function() {
    rebindClicks();
});

The trick here seems to be that, after the update event, the binds are lost, so we have to rebind them after the update. That's  why p:ajax has that oncomplete event.

Finally, when the user clicks on the element from the target list, we call the updateCommand, declared via p:remoteCommand.

Notice that the selected item is passed as a parameter called "param". We get this parameter back in the managed bean like this

public void updateAccounts(){
   String value = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("param");
   System.out.println(">"+value);
   this.accounts = value;
}

In this case, the value is just the ID, because that's what my siteConverter does to the object.

@ManagedBean
@RequestScoped
public class SiteConverter implements Converter,Serializable{

    private static final long serialVersionUID = -194442504109002565L;

    @EJB
    private MyEJB myEJB;

    @Override
    public Object getAsObject(FacesContext arg0, UIComponent arg1, String arg2)
            throws ConverterException {
        if (arg2 != null){
            Site s = myEJB.getSiteById(Long.parseLong(arg2));
            return s;
        }else{
            return null;
        }
    }

    @Override
    public String getAsString(FacesContext arg0, UIComponent arg1, Object arg2)
            throws ConverterException {
        if (arg2 != null){
            Site s = (Site)arg2;
            return String.valueOf(s.getId());
        }else{
            return null;
        }
    }
}