Thursday, June 24, 2021

Set up WildFly, PostgreSQL, and Maven with Docker Compose

In many of my previous posts I have resorted to manual installation and configuration of software in my own computer. This was the case of WildFly and MySQL, which cost me more than my fair share of headaches. Manually installing software often runs into troubles when students try to replicate the same setup. In many cases the installation cannot complete  with success on their laptops for some obscure reason. Furthermore, the process of installing and configuring is usually cumbersome and not prone to generalization, as one goes through multiple menus taking different options, downloading drivers and so on.

I changed my mind about this approach some time after deciding to embrace Visual Studio Code (VSC) as my companion text editor (at least for everything related to code, be it LaTeX, Python, or Java). I would say that it still performs clearly below a full-blown IDE, like Eclipse, but the familiarity of the setup is also an important advantage, as I switch from one language to the next. It's a kind of trade-off here. At some point I realized that VSC would let me develop on standard Docker containers, i.e., I could configure an environment inside VSC that I could entirely replicate on another computer with nothing but a couple of configuration lines. This is a remarkable achievement that motivates this post.

Here, I'm presenting a docker compose set up with three containers:
  • The PostgreSQL database.
  • The WildFly application server.
  • And a command line with maven. I could discard one of the containers somehow, e.g., by mounting a volume with WildFly on this last container, but since I'm already needing two containers, adding a third ones comes nearly for free.
For this example to work you need to have Docker installed. I tried this on MacOS and assume that this will work straightforwardly on Linux. Please let me know of your Windows experience.

The docker-compose.yaml

The general idea is to define a yaml compose file, docker-compose.yaml:

version: '3.4'

services:
database:
image: postgres:9
ports:
- 5432:5432
environment:
POSTGRES_USER: postgres # The PostgreSQL user (useful to connect to the database)
POSTGRES_PASSWORD: My01pass # The PostgreSQL password (useful to connect to the database)
POSTGRES_DB: school # The PostgreSQL default database (automatically created at first launch)
wildfly:
build:
context: .
dockerfile: Dockerfile
links:
- database
ports:
- 8080:8080
- 9990:9990
command-line:
image: maven:3-openjdk-17
command: tail -f /dev/null
links:
- database
- wildfly
volumes:
- ..:/workspace

We can see the three services I mentioned. Two of them have the tag "image", which contains a direct reference to the Docker hub container to download (postgres and maven). To prevent the maven container from finishing, we run an interminable script to read from /dev/null. The wildfly service is different, as it builds on a Dockerfile available in the same directory as this yaml file (the "." part). More about this Dockerfile ahead.

One can also see that each one of the containers links to the ones before: wildfly "sees" the database container and may access it using the network name "database", while the command-line service (maven) can see the previous two. I also added port mappings for the database and wildfly, as we want to access the database, and mainly, the WildFly server from our own browser on the very same ports 8080 and 9990.

In this example, I'm setting the environment of the database directly, which is not perfect, because this configuration file provides direct knowledge of the password. It might be better to use environment variables or refer to an external file with the environment configuration. Finally, notice the volume we are mounting in the command-line service: the ".." directory will show up inside the container as "/workspace"; whatever you write starting in the previous directory of your computer will show up in the /workspace and vice-versa. The main idea is that you can compile inside the /workspace in the container, using maven, while keeping everything, including the results outside the container. The container may go, but the results will stay.


The Dockerfile

FROM jboss/wildfly:24.0.0.Final
RUN /opt/jboss/wildfly/bin/add-user.sh admin admin#7rules --silent
ADD --chmod=0755 wildfly-init-config.sh /opt/jboss/wildfly/bin
CMD ["/opt/jboss/wildfly/bin/wildfly-init-config.sh"]

We need this file because the base wildfly container doesn't do everything we need. We must set up an administrator account and a data source. The administrator part is easy, but the data source not so much. The challenge is that we must first start WildFly, make sure that it is running and---only then---install the data source. Hence, we may resort to a shell script called "wildfly-init-config.sh" for that purpose.

The wildfly-init-config.sh script

#! /bin/sh

#start wildfly
/opt/jboss/wildfly/bin/standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0 &

#install the driver and the data source
#the challenge is to have the server ready---it may take a while
RC=1
count=0
while [ $RC -ne 0 ] && [ $count -lt 5 ]
do
sleep 5
/opt/jboss/wildfly/bin/jboss-cli.sh "connect","deploy --force --url=https://jdbc.postgresql.org/download/postgresql-42.2.22.jar","data-source add --name=PostgreDS --driver-name=postgresql-42.2.22.jar --driver-class=org.postgresql.Driver --jndi-name=java:/PostgresDS --connection-url=jdbc:postgresql://database:5432/school --user-name=postgres --password=My01pass"
RC=$?
let count++
done

#keep running forever
tail -f /dev/null

This script starts WildFly in background making sure that the sockets bind to address 0.0.0.0, to enable connection from our own machine. Then, it tries to deploy the PostgreSQL JDBC driver and install a data source, at most 5 times, until it succeeds, because the connect may fail a couple of times while WildFly starts. This gives 25 seconds for WildFly to start. In the end we just hang the script to prevent the container from finishing using no CPU in the process.

Adding everything up

We now need to run a couple of commands on the command line. One to build the images and run the containers:

docker-compose up --build


This may take a while depending on your network speed. Once this finishes, we may try the addresses localhost:8080 and localhost:9990 on the browser, to get the following results, starting from the 8080 port:
The driver:

And the data source that passes the connection test to the PostgreSQL database:


The next command is optional and allows us to enter the container that has maven, such that we can run maven itself. In the end, this was what we were looking for, a fancy way of running maven with minimal installation of software on our own operating system. To do this, we start by looking for the identifiers of the running containers:

docker ps


Then, we look for the identifier of the maven container. In my case it is: 4a229ca55f65. You need to replace the identifier of the container by the identifier of your specific container.

docker exec -it 4a229ca55f65 /bin/sh


You can now run maven having a view of your parent directory ("..") inside this container under the name "workspace":

sh-4.4# ls workspace/

EJB-client  EJB-server JMS  JPA-standalone  KafkaStreams  LICENSE  README.md  application  kafka  local-maven-repo


Another very important feature is to look at the logs, specially of WildFly, as these often give precious indications of what is wrong. For example, a shortcoming of this setup concerns Java versions, as the maven container is ready for Java 17, but the WildFly class loader doesn't seem to accept anything above 11. While Maven was giving me some ambiguous error message, the logs immediately showed the problem. To look at the logs we may use the following command:

docker logs d7882574eb38


being d7882574eb38 the WildFly Docker container.

Note that you must take the versions problem into consideration when compiling the source code. As soon as I changed the Java compiled version to 11 in Maven everything ran smoothly. As I played around a little bit with this version of WildFly, it seemed to me that it is highly unstable and prone to crash. You may, therefore, look for a better version.

Optional Final Step: Visual Studio Code

Instead of doing everything on the command line, you may use this compose set up in IDEs or text editors like VSC. In the case of VSC, this will make part of VSC work inside the container, the VSC server, which is thus able to reach the workspace directory and the tools available in the command line container, namely maven. For this to work, include the following file, named "devcontainer.json" close to the previous files, all inside the same directory named ".devcontainer":

{
"name": "maven+wildfly+postgresql",
"dockerComposeFile": "docker-compose.yml",
"workspaceFolder": "/workspace",
"service": "command-line",
"extensions": [
"dotjoshjohnson.xml",
"vscjava.vscode-maven",
"vscjava.vscode-java-pack",
"redhat.fabric8-analytics"
]
}

Notice the references to the docker-compose.yaml file and to the service. I have also installed a few extensions. Once you issue a "reopen in container", the entire VSC environment will be ready for maven+wildfly+postgresql. You can see the result here (notice the orange rectangle in the lower left corner):


Should you open a terminal, it will be inside the maven container.

Monday, November 18, 2019

REST Web Services with Java Enterprise Edition done with a Maven project

In this post I'm going to do a server-side REST project to deploy on an application server like WildFly. A standard Enterprise JavaBean does all the heavy lifting in this project. We have three classes to take care of data and make the case more interesting. First the Material class:


package data;

import java.io.Serializable;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;


@XmlAccessorType(XmlAccessType.FIELD)
public class Material implements Serializable {
 private static final long serialVersionUID = 1L;

 @XmlAttribute
 int id;
 
 private String title;
 private String location;
 
 public Material() {}
 
 public Material(int id, String name, String location2) {
  this.id = id;
  this.title = name;
  this.location = location2;
 }
 
 public int getId() {
  return id;
 }

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

 public String getTitle() {
  return title;
 }
 public void setTitle(String title) {
  this.title = title;
 }
 public String getLocation() {
  return location;
 }
 public void setLocation(String location) {
  this.location = location;
 }

 public void update(String name, String location2) {
  this.title = name;
  this.location = location2;
 }

}

Each course has materials. I use JAXB annotations, because I want to serialize data as XML (as well as JSON, but I'm using the defaults for JSON):

package data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;


@XmlAccessorType(XmlAccessType.FIELD)
public class Course implements Serializable {
 private static final long serialVersionUID = 1L;
 @XmlAttribute
 private int id;
 private String name;
 @XmlElementWrapper(name="materials")
 @XmlElement(name="material")
 private List<Material> materials;

 public Course() {
  super();
 }

 public Course(int id, String name2) {
  this.name = name2;
  this.id = id;
 }
 
 public int getId() {
  return id;
 }

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

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }


 public void addMaterial(Material material) {
  if (this.materials == null)
   this.materials = new ArrayList<>();
  this.materials.add(material);
 }
 
 public void deleteMaterial(Material material) {
  if (this.materials == null)
   return; //XXX: silent error?
  this.materials.remove(material);
 }

 public List<Material> getMaterials() {
  return this.materials;
 }
   
}

And now a list of courses:

package data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;


@XmlRootElement(name="ListCourses")
public class ListCourses implements Serializable {
 private static final long serialVersionUID = 1L;
 private List<Course> courses;


 public ListCourses() {
  this.courses = new ArrayList<>();
 }

 public List<Course> getCourses() {
  return courses;
 }

 public void setCourses(List<Course> courses) {
  this.courses = courses;
 }

 public void addCourse(Course c) {
  this.courses.add(c);
 }

 public Course get(int id) {
  return this.courses.get(id);
 }

}

I have an Enterprise JavaBean that initializes some data to give back to the REST service. In practice, this EJB would typically go to a database to get the data in need:

package ejb;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;

import data.Course;
import data.ListCourses;
import data.Material;

/**
 * Session Bean implementation class MyBean
 */
@Stateless
@LocalBean
public class MyBean {
 private ListCourses lc;
 
    /**
     * Default constructor. 
     */
    public MyBean() {
  Course courses[] = {new Course(1, "IS"), new Course(2, "ES"), new Course(3, "PPP")};
  Material materials[] = {new Material(1, "book", "/usr"), new Material(2, "slides", "/usr/slides"), new Material(3, "exercises", "/usr/exercises")};
  
  courses[0].addMaterial(materials[0]);
  courses[0].addMaterial(materials[1]);
  courses[1].addMaterial(materials[0]);
  courses[1].addMaterial(materials[2]);
  courses[2].addMaterial(materials[1]);
  courses[2].addMaterial(materials[2]);
  
  lc = new ListCourses();
  for (Course c : courses)
   lc.addCourse(c);
    }
    
    
    public ListCourses getListCourses() {
     return this.lc;
    }
    
    public Course getCourse(int id) {
     return this.lc.get(id);
    }

}

And now the two main classes of the example. First the "Root resource class". Please note the method name (@Get), the path (@Path), the MIME media type of the response (@Produces). I don't have any example with @Consumes, which specifies the MIME media type of the input:

package rest;

import java.util.List;

import javax.enterprise.context.RequestScoped;
//import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import data.Course;
import data.ListCourses;
import ejb.MyBean;

@Path("/project3webservices")
@RequestScoped
public class WebServicesServer {
 
 @Inject
 MyBean db;
 
 public WebServicesServer() throws NamingException {
  System.out.println("WebServicesServer created. db = " + this.db);
 }
 
 // http://localhost:8080/play-REST-server/webapi/project3webservices/gettext
 @GET
 @Path("gettext")
 @Produces({MediaType.TEXT_PLAIN})
 public String getText() {
  return "Hello World!";
 }
  
 // http://localhost:8080/play-REST-server/webapi/project3webservices/getmaterials
 @GET
 @Path("getmaterials")
 @Produces({MediaType.APPLICATION_XML})
 public ListCourses getAllMaterials() {
  return db.getListCourses();
 }
 
 
 // http://localhost:8080/play-REST-server/webapi/project3webservices/getstudents?id=1
 @GET
 @Path("getstudents")
 @Produces({MediaType.APPLICATION_JSON})
 public Course getAllStudents(@QueryParam("courseid") int id) {
  return db.getCourse(id);
 }
}

And then a class to define the root application path:

package rest;

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

@ApplicationPath("webapi")
public class HelloWorldApplication extends Application {
}

Finally, the pom.xml. You may specify the goals (mvn) clean install wildfly:deploy for WildFly. Assuming WildFly is running and configured with the default ports, this project will be immediately deployed.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>is</groupId>
<artifactId>project3-REST-server</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>project3 REST web services</name>
<url>http://maven.apache.org</url>


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.xml.ws/jaxws-rt -->
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.3.2</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>play-REST-server</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>12</source>
<target>12</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>

<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<version>2.0.1.Final</version>
<configuration>
<hostname>localhost</hostname>
<port>9990</port>
</configuration>
</plugin>
</plugins>
</build>

</project>

Then, you may go to a browser to invoke the three URLs I show in the root resource class to get the following results. One should notice the awesome fact that we are getting XML and JSON as requested using nothing more than a couple of annotations. Java EE makes that for us:




Saturday, November 16, 2019

SOAP Web Services with Java Enterprise Edition done with a Maven project

This post is an update of another post I did a few years ago before I used Maven. My goal is to create a simple SOAP Web Service and use it from a client. In theory, the main difficulty is the Maven configuration, but I found it troublesome to successfully conclude the client, due to a persistent problem with the Java versions. Hopefully I can provide some help in the process.

Server

The source code of the server is not very realistic, but this is only for illustration purposes. I wrote the server and completed all the necessary steps on the server with Java 12 and WildFly 18. For the client I was forced to use a different version. Since I am using a Maven project, I started a new project from scratch without any archetype. The project had this structure:



From here I created the Java code for the server:

import java.util.Hashtable;
import java.util.Map;

import javax.jws.WebMethod;
import javax.jws.WebService;


@WebService
public class MyAgenda {
 private static Map<String, String> agenda = new Hashtable<>();
 
 @WebMethod
 public String sayHello(String name) {
  return "Hello " + name;
 }
 
 @WebMethod
 public String getPhone(String name) {
  if (agenda.containsKey(name))
   return agenda.get(name);
  else
   return null;
 }
 
 @WebMethod
 public void setPhone(String name, String phone) {
  agenda.put(name, phone);
 }
}

Next, I need to write the pom.xml file. This has a few important details: the packaging, the ws dependency and the plugins that control the war packaging and the deployment on a WildFly server (keep reading if you don't use WildFly). I configured the file for it to work without a web.xml file inside the webapp directory:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>is</groupId>
<artifactId>play-webservices</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>playwebservices Maven Webapp</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.xml.ws/jaxws-rt -->
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.3.2</version>
<type>pom</type>
</dependency>
</dependencies>
<build>
<finalName>play-webservices</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>12</source>
<target>12</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>

<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<version>2.0.1.Final</version>
<configuration>
<hostname>localhost</hostname>
<port>9990</port>
</configuration>
</plugin>
</plugins>
</build>

</project>

If you use WildFly, once you start the server you may then set the following maven phases:

(mvn) clean install wildfly:deploy

to compile the project, build the war file (into the target directory) and finally deploy the project in WildFly. You may skip this last step to get the war file and deploy it manually in your server (even in WildFly).

Once this is properly set up you should be something like this in the server console:

11:20:16,416 INFO  [org.jboss.ws.cxf.metadata] (MSC service thread 1-7) JBWS024061: Adding service endpoint metadata: id=MyAgenda
 address=http://localhost:8080/play-webservices/MyAgenda
 implementor=MyAgenda
 serviceName={http:///}MyAgendaService
 portName={http:///}MyAgendaPort
 annotationWsdlLocation=null
 wsdlLocationOverride=null

 mtomEnabled=false

You now may go to the browser and look for the following URL: http://localhost:8080/play-webservices/MyAgenda?wsdl

to get this result:




Client

The client, in a different project, is as simple as the server (mind a detail about Java versions below):

import artifact.MyAgenda;
import artifact.MyAgendaService;

public class HelloAppClient {
 private static MyAgendaService service = new MyAgendaService();

 public static void main(String[] args) {
  MyAgenda agenda = service.getMyAgendaPort();
  System.out.println(agenda.sayHello("world"));

  agenda.setPhone("Paula", "9626341");
  
  System.out.println(agenda.getPhone("Paula"));
 }
 
}

Unfortunately, it will not compile because it misses a number of classes that we must generate with the help of the wsimport tool. This brought me to a problem: wsimport ceased to exist in the newer Java versions as a standalone program (refer to my previous version of this example for additional details). We need to execute wsimport via Maven. Sadly, due to a problem I could not use Java 12 and had to revert to Java 8 (which would probably have wsimport by the way...), but kept the principle of using Maven (hopefully this will be fixed in future versions of the jaxws-maven-plugin):

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>is</groupId>
<artifactId>play-webservices-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.xml.ws/jaxws-rt -->
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.3.2</version>
<type>pom</type>
</dependency>
</dependencies>
<build>
<finalName>play-webservices</finalName>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<version>2.5</version>
<executions>
<execution>
<goals>
<goal>wsimport</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- The name of your generated source package -->
<packageName>artifact</packageName>
<wsdlUrls>
<wsdlUrl>
http://127.0.0.1:8080/play-webservices/MyAgenda?wsdl
</wsdlUrl>
</wsdlUrls>
<verbose>true</verbose>
<target>2.1</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>


</plugins>
</build>
</project> 

Once we run Maven with the goal "generate-sources", we get the following additional files (in target/generated-sources/wsimport), possibly after refreshing the IDE:


We are now able to run the project and the get the following (astounding!) results:

Hello world
9626341