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