Azzyzt JEE Tools

Release 1.3.2

Azzyzt JEE Tools is a collection of software tools helping software developers to create software using Java Enterprise Edition 6. It is designed to be integrated into popular Java IDEs.

This file is a tutorial for users of Azzyzt JEE Tools. It has the following sections:

2 Using Azzyzt JEE Tools

3 The cookbook example

4 Using the generated code

5 Examples for REST

6 Customization of the generated code

7 Licenses

Copyright (c) 2011, Municipiality of Vienna, Austria Licensed under the EUPL, Version 1.1 or subsequent versions

2 Using Azzyzt JEE Tools

If you just want to use Azzyzt JEE Tools (as opposed to modify and build them), the recommended way to install the software is via an Eclipse update site. As of release 1.3.2, there is only update site URL, the former generic version. The URL is

http://azzyzt.manessinger.com/azzyzt_generic/

The former edition especially for the Municipiality of Vienna, Austria, the azzyzt_magwien version has always been identical and with the increased level of configurability, the need for a separate version is currently not there.

All announcements of new versions will be published on

http://www.azzyzt.org

When you don’t see any features available from the update site, try unticking “Group items by category”. There actually is a category called “Azzyzt”, but you may see “There are no categorized items” anyway. I believe this to be a bug in Eclipse p2.

Once the feature is installed, make sure to have a Java EE 6 server instance configured. The server does not need to be running, but it must be configured, in order to make the runtime available.

If you want to dig deeper, make your own changes, want to supply patches, etc, the complete sources are available on GitHub under

https://github.com/amanessinger/azzyzt_jee_tools

3 The cookbook example

3.1 Introduction

If you remember my Eclipse / GlassFish / Java EE 6 Tutorial, here we will use more or less the same application, just slightly expanded to show off some new features. All subsequent examples will reference the same database and the same entities. Like in the original tutorial, the sample application will be called “cookbook”, although it still has nothing to do with cooking :)

In the following I frequently reference a subdirectory doc of the Azzyzt source distribution. For your convenience and if you don’t want to fork the source distribution, a compressed archive of this directory (including the sample sources) is available under

http://azzyzt.manessinger.com/doc.zip

and additionally the extracted content is available under

http://azzyzt.manessinger.com/doc/

Everything relevant to this tutorial is in a subdirectory doc/cookbook. Here is its tree structure:

doc/cookbook
|
|-- cookbook---REST-soapui-project.xml
|
|-- oracle
|   |-- README.txt
|   `-- sql
|       |-- create_tables.sql
|       |-- drop_tables.sql
|       |-- initialize_data.sql
|       `-- reinitialize.sql
|
|-- postgresql
|   |-- README.txt
|   `-- sql
|       |-- create_cookbook_db.sql
|       |-- create_cookbook__user.sql
|       |-- create_tables.sql
|       |-- drop_tables.sql
|       |-- initialize_data.sql
|       `-- reinitialize.sql
|
|-- README.txt
|
`-- src
    |-- base
    |   |-- cookbookEJB
    |   |   `-- ejbModule
    |   |       |-- com
    |   |       |   `-- manessinger
    |   |       |       `-- cookbook
    |   |       |           `-- entity
    |   |       |               |-- City.java
    |   |       |               |-- Country.java
    |   |       |               |-- Language.java
    |   |       |               |-- Tour.java
    |   |       |               |-- Visit.java
    |   |       |               `-- Zip.java
    |   |       `-- META-INF
    |   |           `-- persistence.xml
    |   |
    |   `-- cookbookEJBClient
    |       `-- ejbModule
    |           `-- com
    |               `-- manessinger
    |                   `-- cookbook
    |                       `-- entity
    |                           `-- VisitId.java
    `-- optional
        |-- cookbookCxfRestClient
        |   `-- src
        |       |-- com
        |       |   `-- manessinger
        |       |       `-- cookbook
        |       |           `-- service
        |       |               |-- ProtectedCxfRestInterface.java
        |       |               `-- test
        |       |                   |-- CityById.java
        |       |                   |-- CookbookRestTest.java
        |       |                   `-- DeleteCity.java
        |       `-- META-INF
        |           `-- xml
        |               |-- get_austria.xml
        |               |-- nested_expressions.xml
        |               |-- query_with_three_conditions.xml
        |               |-- query_with_two_betweens.xml
        |               `-- sorted_list_of_cities.xml
        |-- cookbookEJB
        |   `-- ejbModule
        |       `-- com
        |           `-- manessinger
        |               `-- cookbook
        |                   |-- meta
        |                   |   `-- Azzyztant.java
        |                   `-- service
        |                       `-- ProtectedBean.java
        `-- cookbookServlets
            `-- src
                `-- com
                    `-- manessinger
                        `-- cookbook
                            `-- service
                                `-- ProtectedDelegator.java

There are two directories with with SQL scripts, one for Oracle, one for PostgreSQL. No other database was tested by me, but all databases supported by JPA should be fine.

Note please, that the sample entity sources may not work with all databases, because they rely on sequences. Thus, if you use another database, you’ll probably have to adapt the entity classes. If you do so, please consider contributing your changes plus DDL scripts to set up the schema, and then I’ll include them with future releases.

Apart from some README files there is also a directory “src” with two subdirectories, “base” and “optional”. After finishing the tutorial you will have a set of database-backed Java EE services accessible via REST, SOAP and even Corba/IIOP, plus a JUnit test suite. The directory “src” contains all source files you would normally need to write for yourself.

In various phases of the tutorial, instead of telling you what to write, I’ll ask you to copy the contents of some subdirectories into your Eclipse workspace.

3.2 Prerequisites

If you want to follow the examples given, I urge you to first try them with GlassFish, even if you ultimately want to deploy your own application on another server. Download and install a recent version of GlassFish and of Eclipse. See readme.html for a list of supported and tested versions.

Download and install drivers for Oracle or PostgreSQL. Whatever database you choose, make sure that the JDBC driver is copied to the glassfish3/glassfish/lib directory of the unpacked server.

Download and install a recent JDK. A JRE runtime environment is enough to run Eclipse, but GlassFish needs a JDK. Again, readme.html tells you what was tested.

In order to interact with GlassFish from within Eclipse, you need to install the Oracle GlassFish Server Tools. In recent versions of Eclipse you can go to the Eclipse Marketplace by choosing

Help / Eclipse Marketplace

You’ll get a search box, there you enter “glassfish”, and from the result list you choose

GlassFish Java EE Application Server Plugin for Eclipse

Alternatively you can install it via

Java EE Perspective / Servers View / Context menu / New / Server / Link: Download additional server adapters

If Eclipse can’t access the Internet, then you may be behind a proxy. Go to

Window / Preferences / General / Network Connections

Choose an Active Provider (Native for Windows, uses browser settings, Manual to set the proxy directly in Eclipse and to probably set a proxy user/password).

One last step is needed, installation of Azzyzt JEE Tools themselves. You can either get them via the Eclipse Marketplace or directly from the update site http://azzyzt.manessinger.com/azzyzt_generic/. Accept the open source license, accept to install unsigned software (yes, I know, I should fix this), and finally restart Eclipse when prompted.

At this step you should have a GlassFish installation, Eclipse running and principally being able to connect to GlassFish, and Azzyzt JEE Tools ready for action.

3.3 Preparation

Now let’s create a server instance. Choose

Java EE Perspective / Servers View / Context menu / New / Server

and select the version of GlassFish that you want to use. Don’t worry if you don’t find GlassFish 3.1.1, just select version 3.1, it will work.

When creating the server instance, on the second page of the wizard, you will have to select a Java Runtime Environment (JRE). There is a link on that page, leading to the Installed JRE preferences. Use that to define a JDK as runtime (the default runtime of Eclipse will most likely be a JRE and thus not enough to run GlassFish).

You find all that in greater depth in my Eclipse/ GlassFish / Java EE 6 Tutorial, I just wanted to mention it.

On the third page of the wizard, when asked for an admin password, I leave it empty. It’s more convenient on a development server and I am behind a corporate firewall anyway. On the fourth and last page we could configure applications to run on the new server instance (i.e. deploy them). There is nothing to run yet, thus we click “Finish”.

Next set up a database. This consists of the following steps:

  1. decide between PostgreSQL and Oracle
  2. create a database and set up an empty database schema
  3. use the SQL files in the database-specific subdirectory of doc/cookbook to create tables and initialize data. If you ever want to start over, there is also a script to reinitialize the database.
  4. use a database-specific tool like “sqlplus”, “PgAdminIII” or a database-agnostic tool like “Toad” to verify that the database works, tables and sequences are there, and that the tables “country”, “city”, “zip” and “lang_table” have initial content.

In Eclipse go to the Database Development Perspective (Window / Open Perspective / Other … / Database Development). In the Data Source Explorer view (tree on the left side) under Database Connections add a new Connection (New from the context menu). Choose your database type to your database. See my Eclipse/ GlassFish / Java EE 6 Tutorial for details.

You also need to set up a database connection pool and a JDBC resource in GlassFish. Regardless of how you call the connection pool, the JDBC resource must be called “jdbc/cookbookdb”. Again, see my Eclipse/ GlassFish / Java EE 6 Tutorial for details and screenshots.

At this point you have all the tools and the environment (server and database) ist set up. Next we will create Eclipse projects for the application.

3.4 Creating an azzyzted project

There are different types of Java projects in Eclipse and there are different ways to structure your functionality into projects. The most simple way for us would be to use a so-called Dynamic Web Project. Today this project type allows you to use Enterprise Java Beans and most other features of Java EE, but there are still some reasons, why one would put EJBs into an EJB Project, for instance that this is the only way to get an EJB Client Project, something you need when you want to call your beans the “traditional” way, i.e. via Corba/IIOP.

When you do it yourself, it is certainly more convenient to use a single project, but when you use a generator, a multi-project setup is OK, especially if it grants you more flexibility in case you ever need it.

Azzyzt JEE Tools create such a multi-project setup, and we call the entirety of these projects an “azzyzted project”.

Let’s do it now. Make sure you are in the “Java EE” perspective. There are three ways to get to the “New Azzyzted JEE Project” wizard:

I normally use the keyboard shortcut, as this is the shortest path.

In the wizard dialog enter a project base name, a package name, and choose the target runtime.

The project base name is a prefix that will be used for the four projects that together make up an azzyzted project.

The package name is actually a prefix as well. All generated Java packages will be below this prefix.

The target runtime is a list of all runtimes used in defined server instances. Thus if you have two servers supporting Java EE 6 running or at least defined in Eclipse, one for GlassFish 3.01, one for 3.1, you will see a list of two runtimes. Choose one of them. This does not mean you can’t run the finished application on another server or server version, it only means that this is the runtime that is used to compile against.

If the list of target runtimes is empty, then you have not yet defined a server. Do so from the Servers view with “New / Server”.

In the context of this tutorial, the project base name is “cookbook”, the package name is “com.manessinger.cookbook”. Use these names, because the sample sources under doc/cookbook rely on that. Try it and you will end up with the following five projects:

azzyzt_tools” is used by Azzyzt itself. In a later version it will become useful for you as well, for now I ask you to simply let it alone :)

The EAR project is an Enterprise Application Project, basically a wrapper around the three server-side Java projects. The artifact of an EAR project is an enterprise archive, for instance “cookbookEAR.ear”, and this is the deployable application.

cookbookEJB” is where we put all application functionality, “cookbookEJBClient” is the EJB client project, its artifact could be distributed in order to allow clients to call EJB functionality via Corba. All datatypes used as parameters or return values of EJB service methods must be definied in the client project.

cookbookServlets” is a Dynamic Web Project. It is used for the REST wrappers around service methods contained in “cookbookEJB”. Additionally it can be used to add any kind and number of servlets. Keep in mind though, that you need to put your logic into the EJB project, in order to have the most options for accessing it. If you stick to that pattern, you can access services via Corba, SOAP and REST. Accesses via Corba and SOAP can even partake in distributed transactions.

The three Java projects will have two source folders each. One of them is always named “generated”, that’s where generated code goes. For the EJB and EJBClient projects the other source folder is “ejbModule”, for the Servlets project it is “src”. These source folders are for manually written code.

In our case the following directories and files will be generated initially:

azzyzt_tools
`-- 1.3.2
    |-- antlr-2.7.7.jar
    |-- commons-io-2.0.1.jar
    |-- org.azzyzt.jee.tools.mwe.jar
    |-- runtime
    |   |-- org.azzyzt.jee.runtime.jar
    |   `-- org.azzyzt.jee.runtime.site.jar
    `-- stringtemplate-3.2.1.jar

cookbookEAR
|-- EarContent
|   `-- META-INF
|       `-- azzyzt.xml
`-- lib
    |-- org.azzyzt.jee.runtime.jar
    `-- org.azzyzt.jee.runtime.site.jar

cookbookEJB
|-- ejbModule
|   |-- com
|   |   `-- manessinger
|   |       `-- cookbook
|   |           |-- entity
|   |           |-- meta
|   |           |   `-- Azzyztant.java
|   |           `-- service
|   |               `-- HelloTestBean.java
|   `-- META-INF
|       |-- ejb-jar.xml
|       |-- MANIFEST.MF
|       |-- persistence.xml
|       `-- sun-ejb-jar.xml
`-- generated

cookbookEJBClient
|-- ejbModule
|   `-- META-INF
|       `-- MANIFEST.MF
`-- generated

cookbookServlets
|-- generated
|-- src
`-- WebContent
    |-- index.jsp
    |-- META-INF
    |   `-- MANIFEST.MF
    `-- WEB-INF
        |-- lib
        `-- sun-web.xml

com.manessinger.cookbook.service.HelloTestBean” is one of the two generated classes that will ever be generated into a source folder meant to hold manually written code. You can keep it or throw it away. It is only generated upon project creation and is meant to make the project instantly deployable and callable.

The bean has one (predictable) method

@LocalBean
@Stateless
@WebService
public class HelloTestBean {

    public String hello(String s) {
        return "Hello "+s;
    }
}

Start the server, deploy the EAR (“Add and Remove” from the context menu of the server) and try it via the service test client built into the GlassFish Administration Console (see “Manual testing via GlassFish web service tester” in my Eclipse / GlassFish / Java EE 6 Tutorial about using the Adminstration Console).

The second class that is generated into the user folder is “com.manessinger.cookbook.meta.Azzyztant”. It is explained later when we talk about customization.

For now you may undeploy the application (“Add and Remove” from the context menu of the server), we’re going to copy files, do some edits and there’s no use in deploying everything to the server immediately.

3.5 Azzyzted Modeling With Entities

Before we go on with the tutorial and actually create code, let’s have a deeper look at entity classes, what you can express and how you do it. This is not a reference chapter about JPA modeling, but it shows off what Azzyzt supports and what additions to standard JPA meta-information are available.

Upon project creation, a package “com.manessinger.cookbook.entity” (given the example) was generated under “cookbookEJB/ejbModule”. Use this package to define your entities.

Entities have to extend “org.azzyzt.jee.runtime.entity.EntityBase<ID>”, where ID is the class of the table’s primary key.

There are two ways to create entities:

Whichever way you go, be prepared to have to do some manual work in this phase. Those problems have been in Dali for quite some time and I have not seen any progress in a year. On the other hand, creating the entities or database schemata is definitely not in the scope of Azzyzt JEE Tools. Eclipse Indigo could have changed this, but so far I did not find time to try it.

Note please, that Azzyzted Modeling With Entities only supports mapping annotations on fields, not on accessor methods. This is not a deeply rooted design decision but just a matter of how it was implemented. Though it would be principally possible to support annotations on accessor methods, I currently see no reason to do so. The general consensus among experts seems to be, that none of the two methods has substantial advantages over the other.

Personally I feel that annotating the fields makes it easier to get an overview, and besides it does not tempt the developer to introduce side effects into getters/setters.

3.5.1 Azzyzt-specific annotations

Azzyzt introduces the following extra annotations, that can be used on entity fields:

@Internal
marks a field as internal. It is mapped, but not exposed to service clients.
@CreateTimestamp, @ModifyTimestamp
marks a field as create/modify timestamp. Field types can be Calendar, Date or String. In case it is a string, the annotations need an attribute “format”, and that string has to be a valid format for java.text.SimpleDateFormat
@CreateUser, @ModificationUser
marks a string field as user name. Determining a user name is by definition a highly site-specific thing, thus we rely on some InvocationMetaInfo being generated at the entry point into the service, and being passed on via the standard javax.transaction.TransactionSynchronizationRegistry
As of release 1.3.2 this is only partially implemented. It works for REST and SOAP but not for Corba, and from a service accessed via REST or SOAP, it currently wouldn’t be passed on to backend services accessed via REST or SOAP. The crucial knowledge about how to extract user information from a javax.interceptor.InvocationContext is left to a standalone EJB that I call a SiteAdapter. Azzyzt comes with a site adapter that uses information supplied in an HTTP header called “x-authenticate-userid”, thus it relies on some authenticating portal or gateway in front of the application server. Later on we will see how to configure this.

3.5.2 Sample entities

Remember the folder doc/cookbook/src? For your convenience I have already provided entities and a persistence.xml, that match the sample databases.

Note that the entities work with Postgresql as well as with Oracle. There is nothing that prevents you from writing entities specific to a certain database system, but on the other hand you should not need to. Try to stay database-agnostic if you can, it gives you one dependency less to care about.

doc/cookbook/src/base
|
|-- cookbookEJB
|   `-- ejbModule
|       |-- com
|       |   `-- manessinger
|       |       `-- cookbook
|       |           `-- entity
|       |               |-- City.java
|       |               |-- Country.java
|       |               |-- Language.java
|       |               |-- Tour.java
|       |               |-- Visit.java
|       |               `-- Zip.java
|       `-- META-INF
|           `-- persistence.xml
|
`-- cookbookEJBClient
    `-- ejbModule
        `-- com
            `-- manessinger
                `-- cookbook
                    `-- entity
                        `-- VisitId.java

In order to use the entities, just copy the two folders

into your workspace and then refresh the projects in Eclipse.

Most likely an error symbol will be shown on the EJB project, specifically on cookbookEJB/META-INF/persistence.xml. The error that I usually get is “The persistence.xml file does not have recognized content”.

Don’t worry, use “Project / Clean … / Clean all projects” and it will go away. In some cases, especially on Indigo, it may even be necessary to close all projects (“Collapse all”, select the four cookbook projects, use “Close Project” from the context menu), open them again (“Open Project”) and then clean all projects. This repeatably works for me.

The cause seems to be an error in Eclipse, and I read in a comment on stackoverflow.com that it will be fixed in Indigo SR2. Let’s see.

3.5.3 A few words about the sample database

The sample database is pretty simple. It’s not meant to reflect any real application, it’s just a utility to show off some common mapping situations.

We have have countries, cities, ZIPs, visits from a ZIP code area to a certain city, guided using a certain language, and all languages used by guides. We have also tours that are offered through a country, and a tour is in a certain language.

A Country has cities and each City is in a certain country. A country has a number of Zip areas and each ZIP area belongs to a certain country. ZIPs don’t necessarily correspond to cities. Country, City and Zip are easy cases. The corresponding tables each have an ID taken from a sequence, and the only relationships that we need to map, can be modeled using @OneToMany and @ManyToOne.

3.5.3.1 A complicated association

Visit is a more complicated case. It maps a three-ended many-to-many association between City, Zip and Language. A city can be the target of visits from many ZIP areas and different people from a certain ZIP area will visit many cities.

Although JPA supports @ManyToMany, we can’t use it. @ManyToMany is for mapping an association between only two entities and also assumes a simple join table containing no extra attributes. In such a case, for instance between only City and Zip, the join table would not need to be mapped at all, we would just have to map the @ManyToMany associations in City and Zip.

When a join table has additional attributes, you always need to map it as an entity. This is easiest when it has its own ID attribute, preferrably taken from a sequence as well. Then you just have to treat it like any other entity.

Unfortunately both things are rare in legacy databases. Join tables frequently have additional attributes, and the concept of an extra ID on the join table looks extremely alien outside of the realm of object-relational mapping.

To cover this common and not well documented case (at least it took me some time to find anything on the Internet and figure it out), Visit has an extra attribute Long totalNumberOfVisitors that acts as a counter. Think of a system where we have to collect some anonymized statistics about visits and langauges.

The corresponding database table has no explicit ID, thus we have to use the idiom of an embedded ID. The embedded ID is a separate class VisitId in the client project. It’s in the client project, because we will have to pass it around as parameter.

Look at the class Visit and note that it has an ID defined as

@EmbeddedId private VisitId id;

with the getter and setter defined like

public VisitId getId() {
    if (id == null) {
        return null;
    }
    VisitId result = new VisitId(id.getFromZipArea(), id.getToCity(), id.getLangUsed());
    return result;
}

public void setId(VisitId id) {
    if (id == null) {
        return;
    }
    this.id = new VisitId(id.getFromZipArea(), id.getToCity(), id.getLangUsed());
}

Additionally we have three @ManyToOne associations.

@Internal @ManyToOne
@JoinColumn(name="from_zip_area", insertable=false, updatable=false)
private Zip fromZipArea;

@Internal @ManyToOne
@JoinColumn(name="to_city", insertable=false, updatable=false)
private City toCity;

@Internal @ManyToOne
@JoinColumn(name="lang_used", insertable=false, updatable=false)
private Language languageUsedByGuide;

We have explicitly named the join columns, because their names can’t be guessed automatically by JPA. We have also marked them as neither insertable nor updatable, because the attributes that get actually written to the database are inside the embedded ID class VisitId.

We have also used Azzyzt’s @Internal annotation, thus these association attributes will not be contained in generated DTOs (but the embedded ID will). The three association attributes just need to be there to give us the information, that there is an association at all.

VisitId is a class with IDs only. It starts like this:

@XmlRootElement(name="visitId")
@Embeddable
public class VisitId implements Serializable {

    private static final long serialVersionUID = 1L;

    @Column(name="from_zip_area", nullable = false)
    private Long fromZipArea;

    @Column(name="to_city", nullable = false)
    private Long toCity;

    @Column(name="lang_used", nullable = false)
    private String langUsed;

    ...

Note that VisitId is a DTO, and thus it is annotated with @XmlRootElement. It is also used as an embedded Id, thus JPA requires the annotation @Embeddable.

Here is a case where we directly use part of an entity as a DTO, but if you think about it, this is only a generalization. For Visit, VisitId is the type of its ID, and we always expose the type of IDs, only that the type is normally Long and therefore does not look suspicious.

In order to be able to use VisitId as a JPA entity’s ID, we have to define hashCode() and equals(), thus I have generated them in Eclipse using “Source / Generate hashCode() and equals …”.

VisitId is not an entity class, but it must be contained in persistence.xml. If it is not, you get a funny error in Visit, stating that “VisitId is not mapped as embeddable”, which is clearly wrong. The @Embeddable annotation is there, only the error message is wrong, it would have to be “VisitId is not contained in the persistence unit” or something like that.

Also note the two constructors. For the purpose of serialization we need a parameterless constructor, for getId() and setId() in Visit, we need a constructor with values for the three fields.

In City we have one opposite end of the association

@OneToMany(mappedBy="toCity")
private List<Visit> visits;

the second opposite end is defined in Zip

@OneToMany(mappedBy="fromZipArea")
private List<Visit> visits;

and the third in Language

@OneToMany(mappedBy="languageUsedByGuide")
private List<Visit> visits;

The pattern always goes like this. After the same pattern you could have a complex relation between two or four or any number of tables. The embedded ID always contains all ID fields (encapsulates the foreign keys), the constructor always takes parameters for all IDs, we always have bi-directional associations towards all participating entities.

3.5.3.2 Create / modification users and timestamps

All entity classes but Language have some fields automatically set by the code that Azzyzt JEE Tools generate:

@CreateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name="create_timestamp")
private Calendar createTimestamp;

@ModificationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name="modification_timestamp")
private Calendar modificationTimestamp;

@CreateUser
@Column(name="create_user")
private String createUser;

@ModificationUser
@Column(name="modification_user")
private String modificationUser;

In order to demonstrate your options for timestamps, I have defined them in Zip as java.util.Date

@CreateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name="create_timestamp")
private Date createTimestamp;

@ModificationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name="modification_timestamp")
private Date modificationTimestamp;

and finally in Visit as string. This is not uncommon in legacy databases, and because there is no standard string representation that we can rely on, I have decided to require a format string as documented for java.text.SimpleDateFormat.

@CreateTimestamp(format="yyyy-MM-dd-HHmmss.SSS")
@Column(name="create_timestamp")
private String createTimestamp;

@ModificationTimestamp(format="yyyy-MM-dd-HHmmss.SSS")
@Column(name="modification_timestamp")
private String modificationTimestamp;

Create timestamps could be easily set via database defaults, for modification timestamps you could use database triggers, but doing it programmatically, we can make sure that they are only set automatically, it’s more portable this way, and it gives Azzyzt a tad more meta information to work with :)

3.5.3.3 A simple lookup table

In the case of Language I have not used automatically set fields. The underlying table is a typical example of a lookup table. You find that in most databases that were not explicitly designed for access by object-oriented languages. The ID is not generated, it is a simple string, in our case a language code like it is used in locale specifications. The only other attribute is the long name of the language.

Using such tables may be a tad less efficient, but it actually brings a big advantage as well: it makes databases more readable. A foreign key in a visit record can be understood without looking up the value in the language table. And even if you don’t agree, there is no way around the fact that we stumble upon this pattern in practically every legacy database.

Here we are, that’s our entity model. Now back to the tutorial.

3.6 Generating a base application

Once you have written your entities or (in case of this tutorial) copied the two directories from doc/cookbook/src/base, you can generate code. In order to do this, use “Azzyzt / Start code generator” from the context menu of the EJB project. That’s it :)

3.7 Structure of the generated code

In our example, the result of code generation looks like this:

cookbookEAR
|
|-- EarContent
|   `-- META-INF
|       `-- azzyzt.xml
`-- lib
    |-- org.azzyzt.jee.runtime.jar
    `-- org.azzyzt.jee.runtime.site.jar

cookbookEJB
|
|-- ejbModule
|   |-- com
|   |   `-- manessinger
|   |       `-- cookbook
|   |           |-- entity
|   |           |   |-- City.java
|   |           |   |-- Country.java
|   |           |   |-- Language.java
|   |           |   |-- Tour.java
|   |           |   |-- Visit.java
|   |           |   `-- Zip.java
|   |           |-- meta
|   |           |   `-- Azzyztant.java
|   |           `-- service
|   |               `-- HelloTestBean.java
|   `-- META-INF
|       |-- ejb-jar.xml
|       |-- MANIFEST.MF
|       |-- persistence.xml
|       `-- sun-ejb-jar.xml
|
`-- generated
    `-- com
        `-- manessinger
            `-- cookbook
                |-- conv
                |   |-- CityConv.java
                |   |-- CountryConv.java
                |   |-- LanguageConv.java
                |   |-- TourConv.java
                |   |-- VisitConv.java
                |   `-- ZipConv.java
                |-- eao
                |   `-- GenericEao.java
                |-- meta
                |   |-- InvocationRegistry.java
                |   |-- SiteAdapter.java
                |   |-- TransactionRollbackHandler.java
                |   |-- TypeMetaInfo.java
                |   `-- ValidAssociationPaths.java
                `-- service
                    |-- CityFullBean.java
                    |-- CityRestrictedBean.java
                    |-- CountryFullBean.java
                    |-- CountryRestrictedBean.java
                    |-- LanguageFullBean.java
                    |-- LanguageRestrictedBean.java
                    |-- ModifyMultiBean.java
                    |-- VisitFullBean.java
                    |-- VisitRestrictedBean.java
                    |-- ZipFullBean.java
                    `-- ZipRestrictedBean.java

cookbookEJBClient
|-- ejbModule
|   |-- com
|   |   `-- manessinger
|   |       `-- cookbook
|   |           `-- entity
|   |               `-- VisitId.java
|   `-- META-INF
|       `-- MANIFEST.MF
|
`-- generated
    `-- com
        `-- manessinger
            `-- cookbook
                |-- dto
                |   |-- CityDto.java
                |   |-- CountryDto.java
                |   |-- DeleteWrapper.java
                |   |-- Dto.java
                |   |-- LanguageDto.java
                |   |-- StoreDelete.java
                |   |-- StoreWrapper.java
                |   |-- TourDto.java
                |   |-- VisitDto.java
                |   `-- ZipDto.java
                `-- service
                    |-- CityFullInterface.java
                    |-- CityRestrictedInterface.java
                    |-- CountryFullInterface.java
                    |-- CountryRestrictedInterface.java
                    |-- LanguageFullInterface.java
                    |-- LanguageRestrictedInterface.java
                    |-- ModifyMultiInterface.java
                    |-- TourFullInterface.java
                    |-- TourRestrictedInterface.java
                    |-- VisitFullInterface.java
                    |-- VisitRestrictedInterface.java
                    |-- ZipFullInterface.java
                    `-- ZipRestrictedInterface.java

cookbookServlets
|-- generated
|   `-- com
|       `-- manessinger
|           `-- cookbook
|               `-- service
|                   |-- CityFullDelegator.java
|                   |-- CityRestrictedDelegator.java
|                   |-- CountryFullDelegator.java
|                   |-- CountryRestrictedDelegator.java
|                   |-- LanguageFullDelegator.java
|                   |-- LanguageRestrictedDelegator.java
|                   |-- ModifyMultiDelegator.java
|                   |-- RESTExceptionMapper.java
|                   |-- RESTInterceptor.java
|                   |-- RESTServlet.java
|                   |-- TourFullDelegator.java
|                   |-- TourRestrictedDelegator.java
|                   |-- VisitFullDelegator.java
|                   |-- VisitRestrictedDelegator.java
|                   |-- ZipFullDelegator.java
|                   `-- ZipRestrictedDelegator.java
|-- src
`-- WebContent
    |-- index.jsp
    |-- META-INF
    |   `-- MANIFEST.MF
    `-- WEB-INF
        |-- lib
        `-- sun-web.xml

Quite a lot of code, and this is only for five tables. Imagine a real project with dozens or hundreds of tables. Sure, this could all be useless rubbish, so let’s look at what we can do with it :)

4 Using the generated code

The first step is to deploy the EAR project. Note please, that you have to modify persistence.xml to refer to a jta-data-source, and that this data source must be defined in the application server. The Eclipse / GlassFish / Java EE 6 Tutorial has a section titled “Specifying the database, testing, SQL log”, that shows how to do this in GlassFish. The sample persistence.xml from doc/cookbook/src/cookbookEJB/ejbModule/META-INF is already set up correctly for a data source name “jdbc/cookbookdb”. Please double-check that you have defined such a JDBC resource in GlassFish and that it points to a JDBC connection pool set up for our database. If so, after creating the azzyzted project, copying over the enitites, refreshing the projects and rebuilding all, and then finally generating code, we are ready to go.

You deploy the application by first starting the Server from Eclipse’s “Servers” view. When the server runs, select “Add and Remove …” from the context menu of the server. Select cookbookEAR from the box labeled “Available” and move it with “Add” to the box labeled “Configured”. Press “Finish” and wait until the server is synchronized.

Now that the application is deployed, you can call the services.

For each table/entity Azzyzt has created a DTO (EJBClient), two EJBs, one for full (rw) and one for restricted (r) access, corresponding remote interfaces in the EJBClient project, and corresponding REST wrappers around the beans (Servlet project). Additionally we get a ModifyMultiBean, a ModifyMultiInterface and a corresponding REST wrapper.

4.1 SOAP

Here are some URLs of WSDLs (service descriptions of SOAP services) for the generated service beans, assuming GlassFish runs on port 8080:

http://localhost:8080/cookbook/CityFullBean?wsdl

http://localhost:8080/cookbook_restricted/CityRestrictedBean?wsdl

http://localhost:8080/cookbook/CountryFullBean?wsdl

http://localhost:8080/cookbook_restricted/CountryRestrictedBean?wsdl

http://localhost:8080/cookbook/LanguageFullBean?wsdl

http://localhost:8080/cookbook_restricted/LanguageRestrictedBean?wsdl

http://localhost:8080/cookbook/TourFullBean?wsdl

http://localhost:8080/cookbook_restricted/TourRestrictedBean?wsdl

http://localhost:8080/cookbook/VisitFullBean?wsdl

http://localhost:8080/cookbook_restricted/VisitRestrictedBean?wsdl

http://localhost:8080/cookbook/ZipFullBean?wsdl

http://localhost:8080/cookbook_restricted/ZipRestrictedBean?wsdl

http://localhost:8080/cookbook/ModifyMultiBean?wsdl

You can use these WSDLs to create client stubs for access via SOAP.

4.2 REST

Get the WADL description of the REST services from

http://localhost:8080/cookbookServlets/REST/application.wadl

Try the services for yourself, for instance with soapUI. Open soapUI, create a new project from the context menu of “Projects” in the tree view on the left pane. Enter “cookbook - REST” as the project name and paste the WADL URL into “Initial WSDL/WADL”. Press “OK” and soapUI will create all REST resources with one sample request each.

Look at the generated tree. The soapUI project has one service, named just like the project. Below this service, there are resources, two for each entity, one “REST/ful”, the other “__REST/ricted”. They wrap the full and the restricted beans. Below each resource you find the operations that it supports. For the restricted variants these are

all()
Delivers a list of all objects of that class. Useful if you intend to put the result into a set or a map. The typical application would be a client-side cache of a small table of values used for display. This is always a GET request.
byId(id)
Takes an ID and delivers the object with that ID. For single-valued IDs this is a GET request with the ID as URL parameter named id, for embedded IDs it is a POST request.
list(query_spec)
Takes a query specification and delivers all objects of the class, that match the query specification. This is always a POST request.

The non-restricted resources have the same operations as the restricted, and additionally the following:

store(dto)
Takes a DTO and stores the corresponding object. If the object already exists in the database, this is an UPDATE, otherwise it is an INSERT. Store returns a DTO for the stored object, to give the client access to server-generated content (IDs, timestamps, usernames, defaults from the database). This is always a POST request.
delete(id)
Takes an ID and deletes the corresponding object. For single-valued IDs this is a GET request with the ID as URL parameter named id, for embedded IDs it is a POST request.

Additionally there is a resource called “modifyMulti”. It has the operations

storeMulti(dtos)
Takes a list of DTOs of any kind, stores them and returns the same list of DTOs in the same order, with server-generated values updated. This is a POST request.
deleteMulti(dtos)
Takes a list of DTOs of any kind, and deletes the corresponding objects. The only requirement for the DTOs is to be of the correct type and have the ID set. All other fields are ignored. This is a POST request.
storeDeleteMulti(storeDelete)
Takes an object with two lists of DTOs, one of objects to be stored, one of objects to be deleted. This is a POST request.

From the operations, drill down to the generated sample requests. Each operation will automatically have one request called “Request 1”.

The operations mentioned so far consume (where applicable) XML input and produce XML output. Additionally for each operation there is also a variant with the suffix Json. Thus for store(dto) there is also a storeJson(dto). It won’t come as a surprise that these variants consume and produce JSON, the JavaScript Object Notation.

4.3 Variants of the query specification

The XML format of query specifications was chosen to be easily creatable from Adobe Flash REST clients. When the services are accessed via SOAP or Corba, the “list()” methods take an object of type QuerySpec as parameter. Alternatively a second method “listByXML()” is generated, and those methods again take XML in form of a string parameter. The XML format is the same as for REST.

5 Examples for REST

I assume that you use soapUI to work through the following examples. In soapUI you send a request by opening it (double click in the tree) and clicking on the green triangle in the upper left corner of the request sub-window. That’s all you have to do for a parameterless GET request (only all()). The result of a request will always be in the right pane of the request sub-window. The URL for the request will already be correct, because the request has been generated from a WADL description.

For GET requests with parameters, you already see the needed parameters in the left pane. This is only the case for “byId” and “delete”, thus the name is always “id”. Fill in the value, press <TAB> to get out of the value field (important, otherwise soapUI does not accept the value!) and send the request.

For POST requests, the left pane has an upper and a lower part. Just copy the XML or JSON from the example and paste it into the lower left pane. Then send the request.

For a POST request you need to set the MIME type (“Media Type”) of the POST data. In soapUI this defaults to “application/xml”. For JSON requests you have to change it to “’‘application/json” manually. It is not contained in the drop-down list, but you can edit the text.

5.1 List of all countries

Issue a GET request to any of the following URLs:

http://localhost:8080/cookbookServlets/REST/ful/country/all

http://localhost:8080/cookbookServlets/REST/ricted/country/all

The result will be an unsorted list of all countries:

<dtoes>
   <country>
      <createTimestamp>2011-06-15T12:59:28.896+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>1</id>
      <modificationTimestamp>2011-06-15T12:59:28.896+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Austria</name>
   </country>
   <country>
      <createTimestamp>2011-06-15T12:59:28.937+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>2</id>
      <modificationTimestamp>2011-06-15T12:59:28.937+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Italy</name>
   </country>
   <country>
      <createTimestamp>2011-06-15T12:59:28.958+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>3</id>
      <modificationTimestamp>2011-06-15T12:59:28.958+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>USA</name>
   </country>
</dtoes>

Issue a GET request to any of the JSON equivalents:

http://localhost:8080/cookbookServlets/REST/ful/country/allJson

http://localhost:8080/cookbookServlets/REST/ricted/country/allJson

and you get the same list, but now in JSON notation:

{"country": [
      {
      "createTimestamp": "2011-06-15T12:59:28.896+02:00",
      "createUser": "admin",
      "id": "1",
      "modificationTimestamp": "2011-06-15T12:59:28.896+02:00",
      "modificationUser": "admin",
      "name": "Austria"
   },
      {
      "createTimestamp": "2011-06-15T12:59:28.937+02:00",
      "createUser": "admin",
      "id": "2",
      "modificationTimestamp": "2011-06-15T12:59:28.937+02:00",
      "modificationUser": "admin",
      "name": "Italy"
   },
      {
      "createTimestamp": "2011-06-15T12:59:28.958+02:00",
      "createUser": "admin",
      "id": "3",
      "modificationTimestamp": "2011-06-15T12:59:28.958+02:00",
      "modificationUser": "admin",
      "name": "USA"
   }
]}

5.2 City with the ID 1

A GET request to any of these URLs

http://localhost:8080/cookbookServlets/REST/ful/city/byId?id=1

http://localhost:8080/cookbookServlets/REST/ricted/city/byId?id=1

delivers

<city>
   <countryId>1</countryId>
   <createTimestamp>2011-06-15T12:59:28.979+02:00</createTimestamp>
   <createUser>admin</createUser>
   <id>1</id>
   <modificationTimestamp>2011-06-15T12:59:28.979+02:00</modificationTimestamp>
   <modificationUser>admin</modificationUser>
   <name>Graz</name>
</city>

or with JSON

http://localhost:8080/cookbookServlets/REST/ful/city/byIdJson?id=1

http://localhost:8080/cookbookServlets/REST/ricted/city/byIdJson?id=1

delivers

{
   "countryId": "1",
   "createTimestamp": "2011-06-15T12:59:28.979+02:00",
   "createUser": "admin",
   "id": "1",
   "modificationTimestamp": "2011-06-15T12:59:28.979+02:00",
   "modificationUser": "admin",
   "name": "Graz"
}

5.3 Sorted list of cities

POST the following XML document

<query_spec>
   <orderBy>
       <fieldName>country.id</fieldName>
       <ascending>true</ascending>
   </orderBy>
   <orderBy>
       <fieldName>name</fieldName>
       <ascending>false</ascending>
   </orderBy>
</query_spec>

into one of these URLs

http://localhost:8080/cookbookServlets/REST/ful/city/list

http://localhost:8080/cookbookServlets/REST/ricted/city/list

to get a list of all cities, but sorted by the ID of their country ascending, and then alphabetically by their name descending. Here’s the result:

<dtoes>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-06-15T12:59:29.041+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>4</id>
      <modificationTimestamp>2011-06-15T12:59:29.041+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Wien</name>
   </city>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-06-15T12:59:29.020+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>3</id>
      <modificationTimestamp>2011-06-15T12:59:29.020+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Salzburg</name>
   </city>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-06-15T12:59:28.999+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>2</id>
      <modificationTimestamp>2011-06-15T12:59:28.999+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Linz</name>
   </city>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-06-15T12:59:28.979+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>1</id>
      <modificationTimestamp>2011-06-15T12:59:28.979+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Graz</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.123+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>8</id>
      <modificationTimestamp>2011-06-15T12:59:29.123+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Venezia</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.103+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>7</id>
      <modificationTimestamp>2011-06-15T12:59:29.103+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Roma</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.082+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>6</id>
      <modificationTimestamp>2011-06-15T12:59:29.082+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Firenze</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.061+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>5</id>
      <modificationTimestamp>2011-06-15T12:59:29.061+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Bologna</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-06-15T12:59:29.226+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>12</id>
      <modificationTimestamp>2011-06-15T12:59:29.226+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Washington</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-06-15T12:59:29.205+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>11</id>
      <modificationTimestamp>2011-06-15T12:59:29.205+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>New York</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-06-15T12:59:29.165+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>10</id>
      <modificationTimestamp>2011-06-15T12:59:29.165+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Los Angeles</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-06-15T12:59:29.144+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>9</id>
      <modificationTimestamp>2011-06-15T12:59:29.144+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Atlanta</name>
   </city>
</dtoes>

For JSON you still post an XML document, in fact the same XML document, into one of these URLs

http://localhost:8080/cookbookServlets/REST/ful/city/listJson

http://localhost:8080/cookbookServlets/REST/ricted/city/listJson

and get

{"city": [
      {
      "countryId": "1",
      "createTimestamp": "2011-06-15T12:59:29.041+02:00",
      "createUser": "admin",
      "id": "4",
      "modificationTimestamp": "2011-06-15T12:59:29.041+02:00",
      "modificationUser": "admin",
      "name": "Wien"
   },
      {
      "countryId": "1",
      "createTimestamp": "2011-06-15T12:59:29.020+02:00",
      "createUser": "admin",
      "id": "3",
      "modificationTimestamp": "2011-06-15T12:59:29.020+02:00",
      "modificationUser": "admin",
      "name": "Salzburg"
   },
      {
      "countryId": "1",
      "createTimestamp": "2011-06-15T12:59:28.999+02:00",
      "createUser": "admin",
      "id": "2",
      "modificationTimestamp": "2011-06-15T12:59:28.999+02:00",
      "modificationUser": "admin",
      "name": "Linz"
   },
      {
      "countryId": "1",
      "createTimestamp": "2011-06-15T12:59:28.979+02:00",
      "createUser": "admin",
      "id": "1",
      "modificationTimestamp": "2011-06-15T12:59:28.979+02:00",
      "modificationUser": "admin",
      "name": "Graz"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.123+02:00",
      "createUser": "admin",
      "id": "8",
      "modificationTimestamp": "2011-06-15T12:59:29.123+02:00",
      "modificationUser": "admin",
      "name": "Venezia"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.103+02:00",
      "createUser": "admin",
      "id": "7",
      "modificationTimestamp": "2011-06-15T12:59:29.103+02:00",
      "modificationUser": "admin",
      "name": "Roma"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.082+02:00",
      "createUser": "admin",
      "id": "6",
      "modificationTimestamp": "2011-06-15T12:59:29.082+02:00",
      "modificationUser": "admin",
      "name": "Firenze"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.061+02:00",
      "createUser": "admin",
      "id": "5",
      "modificationTimestamp": "2011-06-15T12:59:29.061+02:00",
      "modificationUser": "admin",
      "name": "Bologna"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-06-15T12:59:29.226+02:00",
      "createUser": "admin",
      "id": "12",
      "modificationTimestamp": "2011-06-15T12:59:29.226+02:00",
      "modificationUser": "admin",
      "name": "Washington"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-06-15T12:59:29.205+02:00",
      "createUser": "admin",
      "id": "11",
      "modificationTimestamp": "2011-06-15T12:59:29.205+02:00",
      "modificationUser": "admin",
      "name": "New York"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-06-15T12:59:29.165+02:00",
      "createUser": "admin",
      "id": "10",
      "modificationTimestamp": "2011-06-15T12:59:29.165+02:00",
      "modificationUser": "admin",
      "name": "Los Angeles"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-06-15T12:59:29.144+02:00",
      "createUser": "admin",
      "id": "9",
      "modificationTimestamp": "2011-06-15T12:59:29.144+02:00",
      "modificationUser": "admin",
      "name": "Atlanta"
   }
]}

Stop it, you say, didn’t you pretend that in the JSON variant it’s all JSON? Why XML?

Well, you still have to post the XML with MIME type “application/json”. Fact is, that the server expects a single string parameter, and that this string must contain a valid query specification in XML. Thus the XML of this string parameter is not a matter of transport protocol, it is the payload, it is content.

This single string happens to have the same network representation, regardless of how it is sent, as “application/xml” or as “application/json”. The only difference is the Content-Type header. Otoh, the server expects “application/json” and will produce an error if it gets the wrong content type header.

But again: why XML? Why not have an alternative representation of the query in JSON?

The reason is, that JSON is a format that’s easy to turn into JavaScript objects by calling eval() (though you shouldn’t do that), but it is not a very readable format for people. In the following we will see some XML that could of course be expressed in JSON as well, but that is generally more readable in XML. While readability is subjective, it is matter of fact that XML has wide-spread tool support (editors, syntax highlighting, validation tools, etc), while support for JSON is comparatively sparse.

Apart from that, the generated server side would have to implement a JSON parser for converting JSON specifications in actual queries, just like it currently does with an XML parser. There is no clear advantage in doing that, and therefore the specifications are XML only.

5.4 Query with three conditions

POST the following XML document

<query_spec>
   <expr type="AND">
       <cond type="STRING" op="EQ" caseSensitive="true">
          <fieldName>country.name</fieldName>
          <value>Italy</value>
       </cond>
       <cond type="STRING" op="LIKE" negated="true" caseSensitive="false">
          <fieldName>name</fieldName>
          <value>r%</value>
       </cond>
       <cond type="LONG" op="EQ" negated="true">
          <fieldName>id</fieldName>
          <value>8</value>
       </cond>
   </expr>
   <orderBy>
       <fieldName>name</fieldName>
       <ascending>true</ascending>
   </orderBy>
</query_spec>

into either of

http://localhost:8080/cookbookServlets/REST/ful/city/list

http://localhost:8080/cookbookServlets/REST/ricted/city/list

to get a list of all cities, where the country name equals “Italy” and the city’s name does not begin with “r” (regardless case, this excludes “Roma”), but not the city with the ID 8 (which would have been “Venezia”).

<dtoes>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.061+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>5</id>
      <modificationTimestamp>2011-06-15T12:59:29.061+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Bologna</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.082+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>6</id>
      <modificationTimestamp>2011-06-15T12:59:29.082+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Firenze</name>
   </city>
</dtoes>

Alternatively use the JSON URLs

http://localhost:8080/cookbookServlets/REST/ful/city/listJson

http://localhost:8080/cookbookServlets/REST/ricted/city/listJson

and get this result:

{"city": [
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.061+02:00",
      "createUser": "admin",
      "id": "5",
      "modificationTimestamp": "2011-06-15T12:59:29.061+02:00",
      "modificationUser": "admin",
      "name": "Bologna"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.082+02:00",
      "createUser": "admin",
      "id": "6",
      "modificationTimestamp": "2011-06-15T12:59:29.082+02:00",
      "modificationUser": "admin",
      "name": "Firenze"
   }
]}

The XML-based query language currently supports the unary expression of type “NOT”, as well as the n-ary expressions of type “AND” and “OR”.

n-ary expressions may contain any number of expressions and conditions freely mixed. There is no limit to the level of nesting of expressions.

Conditions have a type and an operator. Supported types are

STRING, SHORT, INTEGER, LONG, FLOAT, DOUBLE

Supported operators are

LIKE, EQ, LT, LE, GT, GE, BETWEEN

where “LIKE” is only supported for type “STRING” and “BETWEEN” is special as it needs not one value but two (see next section).

Field names in conditions or order by clauses can be cross-table references in the same dotted style as they are used in the Java Persistence Query Language (JPQL). Only references along mapped associations are valid. “City” has a field

@ManyToOne
private Country country;

and thus you can follow the association with “country.name”.

5.5 Query with two BETWEENs

POST the following XML document

<query_spec>
   <expr type="OR">
      <cond type="LONG" op="BETWEEN">
         <fieldName>id</fieldName>
         <value>2</value>
         <value2>5</value2>
      </cond>
      <cond type="STRING" op="BETWEEN" caseSensitive="false">
         <fieldName>name</fieldName>
         <value>Linz</value>
         <value2>Salzburg</value2>
      </cond>
   </expr>
   <orderBy>
       <fieldName>id</fieldName>
       <ascending>true</ascending>
   </orderBy>
</query_spec>

into either of

http://localhost:8080/cookbookServlets/REST/ful/city/list

http://localhost:8080/cookbookServlets/REST/ricted/city/list

to get a list of all cities, where either the ID is between 2 and 5 or the name is between “Linz” and “Salzburg”.

<dtoes>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-11-24T13:05:04.491+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>2</id>
      <modificationTimestamp>2011-11-24T13:05:04.491+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Linz</name>
   </city>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-11-24T13:05:04.512+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>3</id>
      <modificationTimestamp>2011-11-24T13:05:04.512+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Salzburg</name>
   </city>
   <city>
      <countryId>1</countryId>
      <createTimestamp>2011-11-24T13:05:04.532+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>4</id>
      <modificationTimestamp>2011-11-24T13:05:04.532+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Wien</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-11-24T13:05:04.553+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>5</id>
      <modificationTimestamp>2011-11-24T13:05:04.553+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Bologna</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-11-24T13:05:04.594+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>7</id>
      <modificationTimestamp>2011-11-24T13:05:04.594+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Roma</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-11-24T13:05:04.656+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>10</id>
      <modificationTimestamp>2011-11-24T13:05:04.656+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Los Angeles</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-11-24T13:05:04.676+01:00</createTimestamp>
      <createUser>admin</createUser>
      <id>11</id>
      <modificationTimestamp>2011-11-24T13:05:04.676+01:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>New York</name>
   </city>
</dtoes>

Alternatively use the JSON URLs

http://localhost:8080/cookbookServlets/REST/ful/city/listJson

http://localhost:8080/cookbookServlets/REST/ricted/city/listJson

and get this result:

{"city": [
      {
      "countryId": "1",
      "createTimestamp": "2011-11-24T13:05:04.491+01:00",
      "createUser": "admin",
      "id": "2",
      "modificationTimestamp": "2011-11-24T13:05:04.491+01:00",
      "modificationUser": "admin",
      "name": "Linz"
   },
      {
      "countryId": "1",
      "createTimestamp": "2011-11-24T13:05:04.512+01:00",
      "createUser": "admin",
      "id": "3",
      "modificationTimestamp": "2011-11-24T13:05:04.512+01:00",
      "modificationUser": "admin",
      "name": "Salzburg"
   },
      {
      "countryId": "1",
      "createTimestamp": "2011-11-24T13:05:04.532+01:00",
      "createUser": "admin",
      "id": "4",
      "modificationTimestamp": "2011-11-24T13:05:04.532+01:00",
      "modificationUser": "admin",
      "name": "Wien"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-11-24T13:05:04.553+01:00",
      "createUser": "admin",
      "id": "5",
      "modificationTimestamp": "2011-11-24T13:05:04.553+01:00",
      "modificationUser": "admin",
      "name": "Bologna"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-11-24T13:05:04.594+01:00",
      "createUser": "admin",
      "id": "7",
      "modificationTimestamp": "2011-11-24T13:05:04.594+01:00",
      "modificationUser": "admin",
      "name": "Roma"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-11-24T13:05:04.656+01:00",
      "createUser": "admin",
      "id": "10",
      "modificationTimestamp": "2011-11-24T13:05:04.656+01:00",
      "modificationUser": "admin",
      "name": "Los Angeles"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-11-24T13:05:04.676+01:00",
      "createUser": "admin",
      "id": "11",
      "modificationTimestamp": "2011-11-24T13:05:04.676+01:00",
      "modificationUser": "admin",
      "name": "New York"
   }
]}

5.6 Nested expressions

An example of a query specification with nested expressions is this:

<query_spec>
   <expr type="OR">
       <cond type="STRING" op="EQ" caseSensitive="true">
          <fieldName>country.name</fieldName>
          <value>Italy</value>
       </cond>
       <expr type="AND">
           <cond type="STRING" op="LIKE" caseSensitive="false">
              <fieldName>name</fieldName>
              <value>l%</value>
           </cond>
           <cond type="LONG" op="EQ" negated="true">
              <fieldName>id</fieldName>
              <value>2</value>
           </cond>
       </expr>
   </expr>
   <orderBy>
       <fieldName>name</fieldName>
       <ascending>true</ascending>
   </orderBy>
</query_spec>

POST it into

http://localhost:8080/cookbookServlets/REST/ful/city/list

http://localhost:8080/cookbookServlets/REST/ricted/city/list

to get a list of all cities in Italy and all other cities beginning with “l” (regardless case), but not the one with ID 2 (which would have been “Linz”).

<dtoes>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.061+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>5</id>
      <modificationTimestamp>2011-06-15T12:59:29.061+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Bologna</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.082+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>6</id>
      <modificationTimestamp>2011-06-15T12:59:29.082+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Firenze</name>
   </city>
   <city>
      <countryId>3</countryId>
      <createTimestamp>2011-06-15T12:59:29.165+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>10</id>
      <modificationTimestamp>2011-06-15T12:59:29.165+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Los Angeles</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.103+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>7</id>
      <modificationTimestamp>2011-06-15T12:59:29.103+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Roma</name>
   </city>
   <city>
      <countryId>2</countryId>
      <createTimestamp>2011-06-15T12:59:29.123+02:00</createTimestamp>
      <createUser>admin</createUser>
      <id>8</id>
      <modificationTimestamp>2011-06-15T12:59:29.123+02:00</modificationTimestamp>
      <modificationUser>admin</modificationUser>
      <name>Venezia</name>
   </city>
</dtoes>

For JSON use

http://localhost:8080/cookbookServlets/REST/ful/city/listJson

http://localhost:8080/cookbookServlets/REST/ricted/city/listJson

and get

{"city": [
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.061+02:00",
      "createUser": "admin",
      "id": "5",
      "modificationTimestamp": "2011-06-15T12:59:29.061+02:00",
      "modificationUser": "admin",
      "name": "Bologna"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.082+02:00",
      "createUser": "admin",
      "id": "6",
      "modificationTimestamp": "2011-06-15T12:59:29.082+02:00",
      "modificationUser": "admin",
      "name": "Firenze"
   },
      {
      "countryId": "3",
      "createTimestamp": "2011-06-15T12:59:29.165+02:00",
      "createUser": "admin",
      "id": "10",
      "modificationTimestamp": "2011-06-15T12:59:29.165+02:00",
      "modificationUser": "admin",
      "name": "Los Angeles"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.103+02:00",
      "createUser": "admin",
      "id": "7",
      "modificationTimestamp": "2011-06-15T12:59:29.103+02:00",
      "modificationUser": "admin",
      "name": "Roma"
   },
      {
      "countryId": "2",
      "createTimestamp": "2011-06-15T12:59:29.123+02:00",
      "createUser": "admin",
      "id": "8",
      "modificationTimestamp": "2011-06-15T12:59:29.123+02:00",
      "modificationUser": "admin",
      "name": "Venezia"
   }
]}

5.7 Store a new record

So far we have not modified anything. Create and modification timestamps have been from the time of database initialization, create and modification user has been “admin”, a value coded into initialize_data.sql, the script that you have used to initialize the empty tables with sample data.

In order to store a new city, use this URL

http://localhost:8080/cookbookServlets/REST/ful/city/store

and POST an XML representation of a city, just as you may have got it from one of the reading operations, but in input data leave the id and the server-generated fields empty. A new object will be created and its complete representation, including server-generated fields, will be returned. Thus POSTing

<city>
   <countryId>2</countryId>
   <id></id>
   <name>Udinw</name>
</city>

may return

<city>
   <countryId>2</countryId>
   <createTimestamp>2011-06-15T15:48:45.503+02:00</createTimestamp>
   <createUser>anonymous</createUser>
   <id>13</id>
   <modificationTimestamp>2011-06-15T15:48:45.503+02:00</modificationTimestamp>
   <modificationUser>anonymous</modificationUser>
   <name>Udinw</name>
</city>

The new id will have been automatically allocated from a sequence.

Let’s try the same with another bogus city name, but this time in JSON. We use

http://localhost:8080/cookbookServlets/REST/ful/city/storeJson

and now the text that we send to the server is actually JSON, not XML. The server does not expect a string, it expects a city object, and it expects this object in JSON transport. Thus we send

{
    "countryId" : "2",
    "name" : "Anconx"
}

and receive

{
   "countryId": "2",
   "createTimestamp": "2011-06-15T18:05:39.272+02:00",
   "createUser": "anonymous",
   "id": "14",
   "modificationTimestamp": "2011-06-15T18:05:39.272+02:00",
   "modificationUser": "anonymous",
   "name": "Anconx"
}

5.8 Update a record, specify user

5.8.1 User names

You may have noticed, that “Udinw” and “Anconx” have been inserted with not only an id set on server side, they had also create/modify timestamps set as well as create/modify users. Both user names were set to “anonymous”.

The code generated by Azzyzt JEE Tools uses an extra EJB to figure out what the user name for a specific request is. I call this a Site Adapter, because it is used to encapsulate a site’s authentication technology and policy.

The site adapter that comes with Azzyzt JEE Tools is called via Inteceptors, and it is called regardless of the invocation method, but currently it is useful only for requests coming via HTTP, either SOAP or REST.

What happens is, that the interceptors pass the request data to the site adapter, and the site adapter tries to take the user name (which is expected to have been authenticated and authorized for access by a portal in front of the service) from an HTTP header. The header’s name defaults to “x-authenticate-userid”. This happens to be what we use internally, but the name can be overridden by setting a custom string resource in the application server.

The Eclipse / GlassFish / Java EE 6 Tutorial has a section titled “Configuration via JNDI Custom Resources”, that shows how to set custom resources in GlassFish.

The runtime tries two JNDI names, an application-specific name, and if that fails, a server-global name. This way we can have different settings in different applications.

The application-specific name for the application “cookbook” would be “custom/stringvalues/app_cookbook/http/header/username”, its type must be java.lang.String and it has to have a property with name “value” and the value of the property should be whatever your HTTP header is called, for instance “x-portal-username”.

If that JNDI string resource is not found, the server-wide resource “custom/stringvalues/http/header/username” is looked up, again its type must be java.lang.String and it has to have a property with name “value” and the value of the property should be whatever your HTTP header is called.

While we are at it, “anonymous”, as the name of the username when no header is given, is also only a default. There is a similar override via JNDI string resources. Again it can be defined per application or for the whole server. The name of the resource must be “custom/stringvalues/app_cookbook/username/anonymous” or “custom/stringvalues/username/anonymous”. Again you need to define a property with name “value”, having the string as value, that you want to use, for instance “unknown”.

After creating or changing those string resources, you have to restart the application!

In soapUI you can set HTTP headers from the request window. At the bottom of the left pane you find text “Headers (0)”, and this text is actually a button. Click on it and the left pane splits horizontally. In the lower part you see an empty list of name/value pairs.

Click the “+” to the left of the divider between upper XML input pane and header list. A dialog opens to ask you for the name of the new header. Enter “x-authenticate-userid” and press “OK”. Now the “value” column of the new header is active. Enter your name or whatever you like. I enter “andreas”. Don’t forget to <TAB> out of the value field before you send the request, otherwise the header will not be sent!

Running through our portal would always automatically set the header, but at home or on the train I have no portal, thus I can either manually set the header for every request, or I can fake it by setting the string resource “custom/stringvalues/username/anonymous” in the server to “andreas”. This is what I have made while creating this tutorial.

5.8.2 Updating the record

Then I issue another store request, but this time with the ID specified as the one that was returned for “Udinw”. Let’s say that instead of “Udinw” (which does not exist in Italy) we really meant “Udine”. It’s the beautiful capital of the Italian province Emilia Romagna. We are happy with the ID, but we want the name to be updated.

POST the following XML

<city>
   <countryId>2</countryId>
   <id>13</id>
   <name>Udine</name>
</city>

into

http://localhost:8080/cookbookServlets/REST/ful/city/store

to rename “Udinw” to “Udine”. This is the result, note the modification user and timestamp, while create user and timestamp have stayed unchanged.

<city>
   <countryId>2</countryId>
   <createTimestamp>2011-06-15T15:48:45.503+02:00</createTimestamp>
   <createUser>anonymous</createUser>
   <id>13</id>
   <modificationTimestamp>2011-06-15T18:10:34.917+02:00</modificationTimestamp>
   <modificationUser>andreas</modificationUser>
   <name>Udine</name>
</city>

Now let’s update Anconx to Ancona via JSON. The URL is

http://localhost:8080/cookbookServlets/REST/ful/city/storeJson

and with the input

{
    "countryId" : 2,
    "id" : 14,
    "name" : "Ancona"
}

we get

{
   "countryId": "2",
   "createTimestamp": "2011-06-15T18:05:39.272+02:00",
   "createUser": "anonymous",
   "id": "14",
   "modificationTimestamp": "2011-06-15T18:15:43.614+02:00",
   "modificationUser": "andreas",
   "name": "Ancona"
}

5.9 Delete a record

Make a GET request to the following URL

http://localhost:8080/cookbookServlets/REST/ful/city/delete?id=13

to delete “Udine”, assuming its ID was indeed “13”. The result will be

<result>OK</result>

and the same with the JSON URL

http://localhost:8080/cookbookServlets/REST/ful/city/deleteJson?id=14

results in

{"result": "OK"}

5.10 Store multiple records in different tables

Frequently one use case will generate more than one object, and in such a case we will want to store them either all together (transactionally safe) or not at all. As far as REST calls go, our unit of transaction is the call itself, thus we have to transport all objects as parameters of the same operation.

Azzyzt JEE Tools generate two stateless session beans for each table, one for full, one for restricted access to records of that table, but so far we have not seen anything for mixed access or for only storing more than one object per call. The special stateless session bean ModifyMultiBean fills that gap. It offers three operations.

POST the following XML

<dtoes>
   <country>
      <id>-1</id>
      <name>France</name>
   </country>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Marseille</name>
   </city>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Paris</name>
   </city>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Rennes</name>
   </city>
</dtoes>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/storeMulti

to store a new country named “France” and three of its cities, “Marseille”, “Paris” and “Rennes”. The result will be something like

<dtoes>
   <country>
      <createTimestamp>2011-06-15T18:25:41.427+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>4</id>
      <modificationTimestamp>2011-06-15T18:25:41.427+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>France</name>
   </country>
   <city>
      <countryId>4</countryId>
      <createTimestamp>2011-06-15T18:25:41.427+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>15</id>
      <modificationTimestamp>2011-06-15T18:25:41.427+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Marseille</name>
   </city>
   <city>
      <countryId>4</countryId>
      <createTimestamp>2011-06-15T18:25:41.427+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>16</id>
      <modificationTimestamp>2011-06-15T18:25:41.427+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Paris</name>
   </city>
   <city>
      <countryId>4</countryId>
      <createTimestamp>2011-06-15T18:25:41.427+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>17</id>
      <modificationTimestamp>2011-06-15T18:25:41.427+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Rennes</name>
   </city>
</dtoes>

Note that “France” did not exist before this call, thus we could not properly reference its ID (now “4”, as seen in the result). Thus instead of leaving the ID empty, we have used a negative proxy ID. In our example we could have used proxy IDs for all objects in the argument list. This was not necessary though. We need proxy IDs only when we reference them from other objects in the same call.

Proxy IDs have no meaning outside of the call. The only requirements are, that all defined proxy IDs within a call are distinct and that they must have been defined in the object list before they can be used. ModifyMultiBean does not reorder objects, it stores them in the order they are given in the list of DTOs. Referencing a proxy ID that has not yet been defined in the list, makes the whole call fail with an InvalidProxyIdException, the transaction is rolled back and nothing is stored at all.

Please note that you can freely mix inserts and updates in the same call to storeMulti(dtos). If something has and ID and an object with that ID exists, then it is an update, if no object with the ID exists, you will get an EntityNotFoundException, and if you don’t send an ID, a new object is inserted.

As for JSON, it seems that we run into a problem with polymorphism though. I would have expected an input of

{
   "country":    {
      "id": -1,
      "name": "Deutschland"
   },
   "city": [
      {
         "countryId": -1,
         "name": "Berlin"
      },
      {
         "countryId": -1,
         "name": "Frankfurt"
      },
      {
         "countryId": -1,
         "name": "Hamburg"
      },
   ]
}

to be the correct equivalent in JSON, at least that’s what I get when I construct a GET operation “List<Dto> test()” and return an ArrayList<Dto> with a country and three cities.

Serialized from Java to JSON it works, but when I try to use the same as input for a parameter that’s also a “List<Dto>”, then the client times out, the operation in the server is never entered, and after some time GlassFish logs a message about having interrupted an idle thread.

There may be another JSON input that works, but so far I must conclude that either JSON can’t deserialize polymorphic lists, or that I trigger a bug in GlassFish’s REST library Jersey. This is with GlassFish 3.1, which ships with Jersey 1.5 and Jackson 1.5.5 as JSON API. As far as I can tell from conversations on the Internet, Jackson 1.7 still has some problems, and the proposed solutions involve the use of annotations that are not part of the Java EE 6 standard. I guess it’s simply a tad too early for polymorphic JSON input.

5.11 Delete multiple records from different tables

The second operation in ModifyMultiBean deletes all in a list of objects.

POST the following XML

<dtoes>
   <city>
      <id>15</id>
   </city>
   <city>
      <id>16</id>
   </city>
   <city>
      <id>17</id>
   </city>
   <country>
      <id>4</id>
   </country>
</dtoes>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/deleteMulti

Again the result will be

<result>OK</result>

and with only one transactional operation we got rid of France.

The problem of correct JSON input (if it exists at all) is still open.

5.12 Store and/or delete multiple records in different tables

The third operation in ModifyMultiBean is slightly more complicated but also more powerful. It can insert/update/delete in one single call. Let’s re-create France by inserting it again:

One more time POST the following XML

<dtoes>
   <country>
      <id>-1</id>
      <name>France</name>
   </country>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Marseille</name>
   </city>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Paris</name>
   </city>
   <city>
      <countryId>-1</countryId>
      <id></id>
      <name>Rennes</name>
   </city>
</dtoes>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/storeMulti

and expect a result like

<dtoes>
   <country>
      <createTimestamp>2011-06-15T19:52:58.569+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>5</id>
      <modificationTimestamp>2011-06-15T19:52:58.569+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>France</name>
   </country>
   <city>
      <countryId>5</countryId>
      <createTimestamp>2011-06-15T19:52:58.569+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>18</id>
      <modificationTimestamp>2011-06-15T19:52:58.569+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Marseille</name>
   </city>
   <city>
      <countryId>5</countryId>
      <createTimestamp>2011-06-15T19:52:58.569+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>19</id>
      <modificationTimestamp>2011-06-15T19:52:58.569+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Paris</name>
   </city>
   <city>
      <countryId>5</countryId>
      <createTimestamp>2011-06-15T19:52:58.569+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>20</id>
      <modificationTimestamp>2011-06-15T19:52:58.569+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Rennes</name>
   </city>
</dtoes>

France and its cities have different IDs now, because IDs come from a database sequence and sequence values don’t repeat.

Now we can delete France again and insert Egypt. POST the following XML

<storedelete>
   <delete>
      <dtoes>
         <city>
            <id>18</id>
         </city>
         <city>
            <id>19</id>
         </city>
         <city>
            <id>20</id>
         </city>
         <country>
            <id>5</id>
         </country>
      </dtoes>
   </delete>
   <store>
      <dtoes>
         <country>
            <id>-1</id>
            <name>Egypt</name>
         </country>
         <city>
            <countryId>-1</countryId>
            <id></id>
            <name>Assuan</name>
         </city>
         <city>
            <countryId>-1</countryId>
            <id></id>
            <name>Cairo</name>
         </city>
      </dtoes>
   </store>
</storedelete>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/storeDeleteMulti

Make sure the first list, the list of objects to delete, contains the exact IDs that you got in the last step, i.e. the country ID of “France” and the IDs of its cities. The Result will be something like

<dtoes>
   <country>
      <createTimestamp>2011-06-15T20:01:33.808+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>6</id>
      <modificationTimestamp>2011-06-15T20:01:33.808+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Egypt</name>
   </country>
   <city>
      <countryId>6</countryId>
      <createTimestamp>2011-06-15T20:01:33.808+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>21</id>
      <modificationTimestamp>2011-06-15T20:01:33.808+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Assuan</name>
   </city>
   <city>
      <countryId>6</countryId>
      <createTimestamp>2011-06-15T20:01:33.808+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>22</id>
      <modificationTimestamp>2011-06-15T20:01:33.808+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Cairo</name>
   </city>
</dtoes>

We have deleted all of “France” and created “Egypt”, this time with two cities, “Assuan” and “Cairo”.

Again you can use the same operations to make updates. In fact you can mix creating new objects and updating others, that already exist.

Each of the two lists could have been left empty, reducing the operation “storeDeletMulti()” to a more complicated version of either “storeMulti()” or “deleteMulti()”, or if both lists are empty, to a very complicated no-op.

Again the question of correct JSON input is open.

5.13 A visit from Austria to Luxor

Remember the strange entity Visit? It represents a many-to-many association between ZIP areas and cities, meaning that a number of visitors from a certain ZIP area have visited a certain city. The number of visitors and the language used by the guide augment the join table and so do our usual create/modification timestamps/users, only that this time the timestamps are actually strings. Additionally the join table has no explicit ID, thus we had to use an embedded ID.

Let’s pretend that we just got statistical data. Five visitors from the Austrian ZIP area Graz-Webling have visited Luxor in Egypt. The guide’s language was British English.

We don’t have “Luxor” so far. This is a case for storeMulti() and for using negative proxy IDs. The database was initialized to have “en_US”, but given Egypt’s British heritage, it seems perfectly plausible that our guide uses British English. We don’t have that language either, so we store it in the same operation. Languages have string IDs, thus we don’t need a proxy ID for the language. The only requirement is, that it is in the list in a position before the DTO that uses it.

POST the following XML

<dtoes>
   <city>
      <countryId>6</countryId>
      <id>-1</id>
      <name>Luxor</name>
   </city>
   <language>
      <id>en_UK</id>
      <languageName>English (UK)</languageName>
   </language>
   <visit>
      <id>
         <fromZipArea>1</fromZipArea>
         <toCity>-1</toCity>
         <langUsed>en_UK</langUsed>
      </id>
      <totalNumberOfVisitors>5</totalNumberOfVisitors>
   </visit>
</dtoes>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/storeMulti

The result is

<dtoes>
   <city>
      <countryId>6</countryId>
      <createTimestamp>2011-06-15T20:04:58.693+02:00</createTimestamp>
      <createUser>andreas</createUser>
      <id>23</id>
      <modificationTimestamp>2011-06-15T20:04:58.693+02:00</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <name>Luxor</name>
   </city>
   <language>
      <id>en_UK</id>
      <languageName>English (UK)</languageName>
   </language>
   <visit>
      <createTimestamp>2011-06-15-200458.693</createTimestamp>
      <createUser>andreas</createUser>
      <id>
         <fromZipArea>1</fromZipArea>
         <langUsed>en_UK</langUsed>
         <toCity>23</toCity>
      </id>
      <modificationTimestamp>2011-06-15-200458.693</modificationTimestamp>
      <modificationUser>andreas</modificationUser>
      <totalNumberOfVisitors>5</totalNumberOfVisitors>
   </visit>
</dtoes>

5.14 Deleting the visit

We’ve already covered deleting multiple objects, but we had no example with an embedded ID. Let’s delete the visit to Luxor, Luxor itself and the language used.

POST the following XML

<dtoes>
   <visit>
      <id>
         <fromZipArea>1</fromZipArea>
         <langUsed>en_UK</langUsed>
         <toCity>23</toCity>
      </id>
   </visit>
   <city>
      <id>23</id>
   </city>
   <language>
      <id>en_UK</id>
   </language>
</dtoes>

into

http://localhost:8080/cookbookServlets/REST/ful/modifyMulti/deleteMulti

Again the result will be

<result>OK</result>

6 Customization of the generated code

6.1 Introducing your friendly Azzyztant

There are cases when we want to influence either what features are generated, or how the generated features behave at runtime. The idea is, to create a central class, that controls both. We call this class the “Azzyztant”.

Since release 1.2.0, azzyzted projects are generated with a default Azzyztant in a sub-package meta within the ejbModule source folder of the EJB project, i.e. parallel to the entity package. This is how the Azzyztant is created:

package com.manessinger.cookbook.meta;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import org.azzyzt.jee.runtime.meta.AzzyztantInterface;
import org.azzyzt.jee.runtime.util.AuthorizationInterface;
import org.azzyzt.jee.runtime.util.StringConverterInterface;


/**
 * Generated class com.manessinger.cookbook.meta.Azzyztant
 * 
 * This class is only generated if it does not exist. It is intended to be 
 * modified. 
 */
@LocalBean
@Stateless
public class Azzyztant implements AzzyztantInterface {

    /*
     * At runtime, azzyzted projects ask the site adapter for the name of the 
     * user invoking a service. The site adapter is expected to return a name
     * as supplied by a portal in front of the application server, or use any
     * other site-specific means to find out who the user is. The problem is
     * that sometimes user names have special formats (like a Windows domain
     * in front of the actual user name), and some applications may need 
     * another format (e.g. without domain name). Here's your chance to step
     * in between site adapter and runtime library:
     *
     * 'usernameConverter' can be set to an instance of any class that implements
     * StringConverterInterface.
     *
     * ATTENTION: keep this stateless, fast and thread-safe!!! 
     * 
     * This is actually a shared instance that, if not null, is called once 
     * upon any invocation. The runtime won't try to synchronize its call to 
     * 'convert()'. Neither should you.
     */
    private final StringConverterInterface usernameConverter = null;

    /*
     * 'authorizer' can be set to an instance of any class that implements
     * AuthorizationInterface.
     *
     * ATTENTION: keep this stateless, fast and thread-safe!!! 
     * 
     * This is actually a shared instance that, if not null, is called once 
     * upon any invocation. The runtime won't try to synchronize its call to 
     * 'checkAuthorization()'. Neither should you.
     */
    private final AuthorizationInterface authorizer = null;

    public Azzyztant() { super(); }


    @Override
    public StringConverterInterface getUsernameConverter() {
        return usernameConverter;
    }

    @Override
    public AuthorizationInterface getAuthorizer() {
        return authorizer;
    }

}

We go into details across the next couple of sections.

6.2 Transforming user names

The Azzyztant has a final field usernameConverter and a getter method for it. At runtime, when the generated application tries to determine the name of the calling user, the supplied site adapter evaluates an HTTP header that is supposed to have been set by an authenticating/authorizing portal in front of the application. We have learned that the name of this header is even customizable via a string resource definition in the server, but that does not yet mean all is well. What about the content of this header?

In many organizations user names are prefixed by a Windows domain (“domain\username”), but imagine a legacy database that is used not only by our generated services, but also by some legacy applications. For some reports they may want to join tables on the create or modification user field, but expect the names to be without the domain (or wholly in upper-case, or … you get the problem).

What Azzyzt JEE Tools generate is an application, but it is also a framework, and like in all frameworks, there is sometimes the need to have a hook into otherwise fully automatic mechanisms. The site adapter automatically determines the user’s name and passes it on to the persistence system, only that we would want a hook in between, where we could modify the name that is passed on. Azzyztant is the place where such hooks will be put from now on.

In our case, we want to specify a non-null usernameConverter. In order to do this, we create a class that implements the interface StringConverterInterface. I suggest creating a sub-package util in the EJB project, and there we could create the converter like this:

package com.manessinger.cookbook.util;

import org.azzyzt.jee.runtime.util.StringConverterInterface;

/**
 * Converter for usernames originating from clients in a Windows domain.
 * It returns the username part without domain.
 *
 */
public class UsernameConverter implements StringConverterInterface {

    @Override
    public String convert(String in) {
        int backslashIndex = in.indexOf('\\');
        if (backslashIndex != -1 && in.length() > backslashIndex + 1) {
            return in.substring(backslashIndex + 1);
        }
        return in;
    }

}

It has to implement the single method String convert(String in), and in this case the method returns either the non-empty suffix after the first backslash, or just the input. Now in Azzyztant, instead of specifying the usernameConverter as null,

private final StringConverterInterface usernameConverter = null;

we specify it as an instance of our class:

private final StringConverterInterface usernameConverter = new UsernameConverter();

Please note that this is a shared instance between all threads of your application, but of course the supplied code is thread-safe, so it does not matter.

6.3 Cutting back on features

Up to release 1.1.1, the generated code did not compile against the JBoss AS 6.0 runtime, because the interfaces generated in the client project were unchangeably annotated with @Remote, and @Remote is not part of the Java EE 6 web profile. On the other hand, @Remote is only needed if you want to call your services via Corba/IIOP, and very often this will not be the case. Web services are in fashion, so why should we tie the code to GlassFish only, just to be able to potentially use something that we know we won’t use anyway? Some users may want it though, and thus we like @Remote on interfaces to be a configurable feature.

The same could be said of access via SOAP or via REST. If you don’t use it in your application, why should Azzyzt generate it?

In order to cut back on unused features, you can put an annotation on the Azzyztant:

package com.manessinger.cookbook.meta;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import org.azzyzt.jee.runtime.meta.AzzyztantInterface;
import org.azzyzt.jee.runtime.util.AuthorizationInterface;
import org.azzyzt.jee.runtime.util.StringConverterInterface;


@LocalBean
@Stateless
@AzzyztGeneratorOptions(
        cutbacks = {
                AzzyztGeneratorCutback.NoRemoteInterfaces, 
                AzzyztGeneratorCutback.NoSoapServices,
                AzzyztGeneratorCutback.NoRestServicesJson, 
                AzzyztGeneratorCutback.NoRestServicesXml,
        }
)
public class Azzyztant implements AzzyztantInterface {

    private final StringConverterInterface usernameConverter = null;

    private final AuthorizationInterface authorizer = null;

    public Azzyztant() { super(); }


    @Override
    public StringConverterInterface getUsernameConverter() {
        return usernameConverter;
    }

    @Override
    public AuthorizationInterface getAuthorizer() {
        return authorizer;
    }

}

For brevity I have removed the generated comments, but otherwise it is the same class as generated, with one exception: There is an annotation @AzzyztGeneratorOptions on the class, that defined the cutbacks. In this case we specify, that we want no @Remote and no @WebService annotations, no REST services with JSON serialization and no REST services with XML serialization. The resulting applications will only be usable as building blocks for your own classes. Add your own services and use the generated beans, converters and DTOs.

If you turn off both variants of REST, Azzyzt won’t generate the REST servlet either!

6.4 Adding features

Cutting back on features is not the only customization, you can also add features. You do that by adding options.

6.4.1 Adding credential-based authorization

6.4.1.1 Credentials

We have seen that Azzyzt does not even try to solve the problem of authentication, i.e. of determining the principal on whose behalf a request was made. Instead Azzyzt assumes that this is solved either by the application server or by a web server, a gateway, in front of it. It is the job of the site adapter to adapt to the method used.

The site adapter delivered with Azzyzt JEE Tools relies on a gateway or portal in front of the application server. This gateway is assumed to authenticate the user, and to pass the information on to the application server via HTTP headers. We have already seen how the username header can be customized.

Knowing who the user is solves only part of the problem though. We would also like to know what the user is allowed to do in the application. This is called authorization.

Java EE 6 has its own model of authentication / authorization based on annotations in the applications and the concepts of realms, users, groups and roles. It is not particularly complicated, but in the GlassFish implementation it is most useful when you have a complete Oracle Enterprise environment.

I think it would be possible to plug any custon solution into the GlassFish server and make it play well with the Java EE 6 annotations, but keeping that all out of scope makes our job even easier, especially when some authentication / authorization infrastructure is already present.

Azzyzt comes with an option to use “credential-based authorization”. The idea is, that the same gateway that already authenticates the user, also delivers some information about the user’s rights, or as we say here, the user’s “credentials”.

Imagine a gateway that authenticates users and looks them up in an X.500 tree via LDAP. It allows or denies access to the application, based on the user and/or the user’s membership in groups. Groups are hierarchically organized, again in X.500, and for each configured application, access to a list of URL prefixes is granted to a set of principals (users and/or groups).

For each pair of URL prefix and principal granted access, there can be a list of credentials attached. The credentials are again expected to be delivered in form of a single HTTP header. The name of the header can be defined via JNDI resources. Again the runtime tries two resources, an application-specific and a server-wide. For our application these would be “custom/stringvalues/app_cookbook/http/header/credentials” and “custom/stringvalues/http/header/credentials” respectively. Again you need to define a property with name “value”, having the string as value, that you want to use, for instance “x-auth-credentials”.

If no JNDI resources are found, the header name defaults to “x-authorize-roles”, the name of the HTTP header used around here.

If no HTTP header is delivered at runtime, the principal is assumed to have no credentials at all.

The credentials header is a single string that is made up of a list of credentials, separated by semicolons. Each credential may be modified by a list of properties. Properties are name/value pairs given in parens. White-space is optional. Let me give some quick examples:

On the frontend, in the gateway, these credentials have no meaning at all. They are simply strings to be sent when a certain principle accesses a certain URL.

Credentials come in two flavors: credentials supplied by the gateway via HTTP headers and credentials required by the application.

In order to require credentials, we can put an annotation @RequiresCredentials on either an EJB class (which means that all methods require these credentials) and/or an EJB class’ methods. The effect is cumulative. An EJB could require a credential “admin” and one of its methods, the method “dangerous()” could require “senior”. In such a case any access to “dangerous()” would need the cumulative credentials “admin(); senior()” to be supplied.

A request is granted access if at least the credentials and properties required are supplied. Additional credentials and properties are OK. Thus if “admin” is required, a user with credentials “admin; senior” is granted access.

If for instance “admin; senior(rank=2)” is required, a user with only “admin; senior” is denied access. Numeric property values are special. A user with “admin; senior(rank=3)” would also be granted access, although the property values don’t match. The rule for numeric properties is, that supplied property values greater or equal required values are granted access.

This is quite flexible and can be used for all sorts of sophisticated authorization schemes. Try to not go over the top though. Simplicity is a virtue.

6.4.1.2 Credential-based authorization

Look at the following Azzyztant:

package com.manessinger.cookbook.meta;

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

import org.azzyzt.jee.runtime.annotation.AzzyztGeneratorOptions;
import org.azzyzt.jee.runtime.meta.AzzyztGeneratorOption;
import org.azzyzt.jee.runtime.meta.AzzyztantInterface;
import org.azzyzt.jee.runtime.util.AuthorizationInterface;
import org.azzyzt.jee.runtime.util.CredentialBasedAuthorizer;
import org.azzyzt.jee.runtime.util.StringConverterInterface;

@LocalBean
@Stateless
@AzzyztGeneratorOptions(
        options = {
            AzzyztGeneratorOption.AddCredentialBasedAuthorization,
        }
)
public class Azzyztant implements AzzyztantInterface {

    private final StringConverterInterface usernameConverter = null;    
    private final AuthorizationInterface authorizer = new CredentialBasedAuthorizer();
    
    public Azzyztant() { super(); }

    @Override
    public StringConverterInterface getUsernameConverter() {
        return usernameConverter;
    }

    @Override
    public AuthorizationInterface getAuthorizer() {
    return authorizer;
    }
}

It specifies no cutbacks and a single option. The option “AzzyztGeneratorOption.AddCredentialBasedAuthorization” causes the code generator to generate an annotation “@RequiresCredentials("modify()")” on the “store()” and “delete()” methods of the full beans and on the class ModifyMultiBean.

These annotations alone do nothing, but we also specify a non-null authorizer, and the class used, CredentialBasedAuthorizer, a class that comes with the Azzyzt runtime, exactly implements the behavior sketched earlier.

You can add your own service beans and you can require any credentials you like. Just add “@RequiresCredentials()” to your beans and/or methods and use the standard CredentialBasedAuthorizer.

6.4.2 Adding an Apache CXF REST client

In the next example we use a cutback and two options:

package com.manessinger.cookbook.meta;

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

import org.azzyzt.jee.runtime.annotation.AzzyztGeneratorOptions;
import org.azzyzt.jee.runtime.meta.AzzyztGeneratorCutback;
import org.azzyzt.jee.runtime.meta.AzzyztGeneratorOption;
import org.azzyzt.jee.runtime.meta.AzzyztantInterface;
import org.azzyzt.jee.runtime.util.AuthorizationInterface;
import org.azzyzt.jee.runtime.util.CredentialBasedAuthorizer;
import org.azzyzt.jee.runtime.util.StringConverterInterface;

@LocalBean
@Stateless
@AzzyztGeneratorOptions(
        cutbacks = {
                AzzyztGeneratorCutback.NoRemoteInterfaces,
        },
        options = {
            AzzyztGeneratorOption.AddCredentialBasedAuthorization,
            AzzyztGeneratorOption.AddCxfRestClient,
        }
)
public class Azzyztant implements AzzyztantInterface {

    private final StringConverterInterface usernameConverter = null;
    private final AuthorizationInterface authorizer = new CredentialBasedAuthorizer();
    
    public Azzyztant() { super(); }

    @Override
    public StringConverterInterface getUsernameConverter() {
        return usernameConverter;
    }

    @Override
    public AuthorizationInterface getAuthorizer() {
    return authorizer;
    }
}

AzzyztGeneratorOption.AddCxfRestClient” is an option that causes the code generator to create one more project, a REST client project using the Apache CXF REST libraries.

The CXF REST client project is not created by default. It is only created if the code generator finds this option. Once it is created, it is not automatically removed, even if you drop the option. Here is how the generated project looks like:

cookbookCxfRestClient
|
|-- generated
|   `-- com
|       `-- manessinger
|           `-- cookbook
|               `-- service
|                   |-- CityFullCxfRestInterface.java
|                   |-- CityRestrictedCxfRestInterface.java
|                   |-- CountryFullCxfRestInterface.java
|                   |-- CountryRestrictedCxfRestInterface.java
|                   |-- LanguageFullCxfRestInterface.java
|                   |-- LanguageRestrictedCxfRestInterface.java
|                   |-- ModifyMultiCxfRestInterface.java
|                   |-- VisitFullCxfRestInterface.java
|                   |-- VisitRestrictedCxfRestInterface.java
|                   |-- ZipFullCxfRestInterface.java
|                   `-- ZipRestrictedCxfRestInterface.java
|-- lib
|   |-- commons-logging-1.1.1.jar
|   |-- cxf-2.4.1.jar
|   |-- jettison-1.3.jar
|   |-- jsr311-api-1.1.1.jar
|   |-- neethi-3.0.0.jar
|   `-- wsdl4j-1.6.2.jar
`-- src

The project comes with a set of Apache CXF libraries included and the class path set up to use them.

6.4.3 CXF? Don’t we use Jersey?

Java EE defines REST bindings for Java. The standard is called JAX-RS and its public API is restricted to the server side. This does not mean that you can’t use REST clients in Java, it only means that Java EE does not mandate a specific client API. This is expected to arrive with Java EE 7.

On the other hand, implementing a server API is more than half of what you need for a corresponding client API, and for testing purposes you’ll want a client API anyway, and therefore all REST implementations have one. The only problem is, that due to the lack of standardization, the client APIs of the various JAX-RS implementations are all different from each other and we have to choose one of them.

Of course there is a client API in Jersey, the reference implementation of JAX-RS, that is also part of the GlassFish application server. I don’t like it, because I find it verbose and complex. Basically you have to repeat a complex incantation for each call, and the underlying model is protocol-centric.

RESTEasy by JBoss is an alternative. You define an interface for the service, and then you can create a client proxy for this interface. What I don’t like in RESTEasy is the handling of responses. It is not as verbose as Jersey, but it is unnecessarily complex as well.

Apache CXF is the third big player, and they have a verbose API like Jersey, but additionally an extremely elegant and simple proxy-based API. Basically you define an interface, from the interface you create a proxy, and just like in RESTEasy you can then call server methods as methods of this proxy. The difference is in response handling. While other APIs require you to help along with deserialization, require casts, etc, the CXF proxy API simply leaves this to JAX-B. It’s more or less like SOAP. Create a proxy once, and then, upon method calls, get the response automatically deserialized into objects. The only thing you need, are classes for the parameters, annotated with JAX-B annotations, but that’s exactly what we have with the DTOs in our EJB client project.

6.4.4 The generated CXF REST client interfaces

For each REST service we generate a corresponding client interface. Let’s take for example the full city client:

package com.manessinger.cookbook.service;

import com.manessinger.cookbook.dto.CityDto;
import com.manessinger.cookbook.dto.Dto;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;


/**
 * Generated interface com.manessinger.cookbook.service.CityFullCxfRestInterface
 */
@Path(value="city")
public interface CityFullCxfRestInterface {

    @GET
    @Path("byId")
    @Produces(MediaType.APPLICATION_XML)
    public CityDto byId(@QueryParam(value="id") Long id);

    @GET
    @Path("all")
    @Produces(MediaType.APPLICATION_XML)
    public List<Dto> all();

    @POST
    @Path("list")
    @Consumes(MediaType.APPLICATION_XML)
    @Produces(MediaType.APPLICATION_XML)
    public List<Dto> list(String querySpecXml);

    @POST
    @Path("store")
    @Consumes(MediaType.APPLICATION_XML)
    @Produces(MediaType.APPLICATION_XML)
    public CityDto store(CityDto dto);

    @GET
    @Path("delete")
    @Produces(MediaType.APPLICATION_XML)
    public String delete(@QueryParam(value="id") Long id);

    @GET
    @Path("byIdJson")
    @Produces(MediaType.APPLICATION_JSON)
    public CityDto byIdJson(@QueryParam(value="id") Long id);

    @GET
    @Path("allJson")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Dto> allJson();

    @POST
    @Path("listJson")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public List<Dto> listJson(String querySpecXml);

    @POST
    @Path("storeJson")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public CityDto storeJson(CityDto dto);

    @GET
    @Path("deleteJson")
    @Produces(MediaType.APPLICATION_JSON)
    public String deleteJson(@QueryParam(value="id") Long id);

}

As you can see, the annotations look exactly as on server side. Using Apache CXF, this interface can be used to create a proxy implementing the interface. Just as in SOAP, the methods return DTOs and lists of DTOs, polymorphism included.

In order to make this work, the CXF REST client project needs access to the DTOs, thus the CXF REST client project is created with the EJB client project on the build path, and in production, its artifact would need the EJB client JAR on the class path.

6.4.5 A simple CXF client example

Here’s a complete toy client. It prints the name of the city whose ID is given on the command line.

package com.manessinger.cookbook.service.test;

import java.net.MalformedURLException;
import java.net.URL;

import org.apache.cxf.jaxrs.client.JAXRSClientFactory;

import com.manessinger.cookbook.dto.CityDto;
import com.manessinger.cookbook.service.CityFullCxfRestInterface;

public class CityById {

    public static void main(String[] args) {
        
        if (args.length != 2) {
            usageExit();
        }

        URL base = null;
        try {
            base = new URL(args[0]);
        } catch (MalformedURLException e) {
            usageExit();
        }
        
        Long id = null;
        try {
            id = Long.parseLong(args[1]);
        } catch (NumberFormatException e) {
            usageExit();
        }
        
        CityFullCxfRestInterface citySvc =
            JAXRSClientFactory.create(
                base.toExternalForm(), 
                CityFullCxfRestInterface.class
            );
        
        CityDto city = citySvc.byId(id);
        System.out.println("City with ID "+id+" is "+city.getName());
    }

    private static void usageExit() {
        System.err.println("usage: CityById <base-url> <city-id>");
        System.exit(1);
    }
}

As you see, most of the code is parameter checking. As soon as the parameters are checked, we use “JAXRSClientFactory” to create the proxy “citySvc”. Then we can use the proxy to make any number of method calls.

The CXF proxy client API is as SOAPish as it gets in REST. Of course there is still communication over the network, and that means things can go wrong. The client may not be able to reach the service, or the service may throw an exception, for instance because there may be no city with the requested ID. In both cases a WebApplicationException is thrown, either in form of a ClientWebApplicationException if it’s a client-side problem, or a ServerWebApplicationException if the problem was on server side.

6.4.6 In-server unit tests

Why should we bother with Java REST clients anyway? After all, Java has a fully conformant SOAP stack, thus we would be better off by just making SOAP calls or calling the beans via Corba, right?

Well, one good reason is, that we may want to make unit tests for our services. When doing so, we will want to maximize coverage by going through as many layers as possible, and that means to make REST requests. There is no need to test the service beans separately, because the REST wrappers call down into the beans anyway. Sure, this way we don’t test access via SOAP, but SOAP web service functionality is basic server functionality enabled by a single annotation. Not much can possibly go wrong here :)

6.4.6.1 Test sources

The doc directory of the Azzyzt distribution contains an optional source tree.

doc/cookbook/src/optional
|-- cookbookCxfRestClient
|   `-- src
|       |-- com
|       |   `-- manessinger
|       |       `-- cookbook
|       |           `-- service
|       |               |-- ProtectedCxfRestInterface.java
|       |               `-- test
|       |                   |-- CityById.java
|       |                   |-- CookbookRestTest.java
|       |                   `-- DeleteCity.java
|       `-- META-INF
|           `-- xml
|               |-- get_austria.xml
|               |-- nested_expressions.xml
|               |-- query_with_three_conditions.xml
|               |-- query_with_two_betweens.xml
|               `-- sorted_list_of_cities.xml
|-- cookbookEJB
|   `-- ejbModule
|       `-- com
|           `-- manessinger
|               `-- cookbook
|                   |-- meta
|                   |   `-- Azzyztant.java
|                   `-- service
|                       `-- ProtectedBean.java
`-- cookbookServlets
    `-- src
        `-- com
            `-- manessinger
                `-- cookbook
                    `-- service
                        `-- ProtectedDelegator.java

In order to use these sources, make sure that you already have configured the option AzzyztGeneratorOption.AddCxfRestClient and generated code. If so, you should have a project cookbookCxfRestClient with nothing but the generated interfaces of the proxies.

We want to make unit tests, thus we need the JUnit libraries on the build path. From the context menu of cookbookCxfRestClient choose “Build Path / Configure Build Path / Libraries / Add Library / JUnit”. The default is “JUnit 3”, make sure that you choose “JUnit 4”.

Now you can copy the two directories of the optional source tree over your cookbook workspace and refresh the projects.

The unit test expects an initialized test database, thus it is a good idea to run reinitialize.sql from either doc/cookbook/oracle/sql or doc/cookbook/postgresql/sql, depending on the database that you use.

6.4.6.2 Testing credential-based authorization

Note that there is a new service bean ProtectedBean under cookbookEJB/ejbModule and a matching REST delegator under cookbookServlets/src. The service bean is annotated with “@RequiresCredentials("admin")” on the bean and “@RequiresCredentials("senior(rank=2)")” on one of its two methods:

package com.manessinger.cookbook.service;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.jws.WebService;

import org.azzyzt.jee.runtime.annotation.RequiresCredentials;

import com.manessinger.cookbook.meta.EJBInterceptor;

@LocalBean
@Stateless
@WebService(serviceName="cookbook")
@RequiresCredentials("admin")
@Interceptors(EJBInterceptor.class)
public class ProtectedBean {

    public String helloAdmin(String s) {
        return s;
    }
    
    @RequiresCredentials("senior(rank=2)")
    public String helloSeniorAdmin(String s) {
        return s;
    }
}

Both methods return their argument. The point is simply to check whether the methods can be called from clients with different credentials.

Note please that the service bean is annotated @Interceptors(EJBInterceptor.class). This is the interceptor that causes credentials to be checked. All generated service beans carry that annotation. Never forget it on your own service beans!

In order to test credentials, the unit test defines two clients for the protected interface, one with credentials “admin; senior(rank=1)” (the low rank client) and one with “admin; senior(rank=3)” (the high rank client).

In three tests the low rank client calls the non-annotated method and succeeds (because it has “admin”), then the annotated method and fails (because its numeric rank is too low) and finally the high rank client calls the annotated method and succeeds (because its rank is even higher than required).

6.4.6.3 The full unit test

package com.manessinger.cookbook.service.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.JAXB;

import org.apache.cxf.jaxrs.client.Client;
import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
import org.apache.cxf.jaxrs.client.ServerWebApplicationException;
import org.apache.cxf.jaxrs.client.WebClient;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.manessinger.cookbook.dto.CityDto;
import com.manessinger.cookbook.dto.CountryDto;
import com.manessinger.cookbook.dto.Dto;
import com.manessinger.cookbook.dto.LanguageDto;
import com.manessinger.cookbook.dto.StoreDelete;
import com.manessinger.cookbook.dto.TourDto;
import com.manessinger.cookbook.dto.VisitDto;
import com.manessinger.cookbook.entity.VisitId;
import com.manessinger.cookbook.service.CityFullCxfRestInterface;
import com.manessinger.cookbook.service.CountryFullCxfRestInterface;
import com.manessinger.cookbook.service.LanguageFullCxfRestInterface;
import com.manessinger.cookbook.service.ModifyMultiCxfRestInterface;
import com.manessinger.cookbook.service.ProtectedCxfRestInterface;
import com.manessinger.cookbook.service.TourFullCxfRestInterface;
import com.manessinger.cookbook.service.VisitFullCxfRestInterface;
import com.manessinger.cookbook.service.ZipFullCxfRestInterface;

/**
 * A test class that executes all tests from the cookbook tutorial. Some tests
 * are slightly altered, in order to make them robust against different IDs and 
 * different execution order. We assume a freshly set up cookbook database.
 */
public class CookbookRestTest {
    
    private static final String BASE_URI = "http://localhost:8080/cookbookServlets/REST";
    
    private static final String LINZ = "Linz";
    private static final String SALZBURG = "Salzburg";

    private static final String FRANCE = "France";
    private static final String MARSEILLES = "Marseilles";
    private static final String PARIS = "Paris";
    private static final String RENNES = "Rennes";
    
    private static final String EGYPT = "Egypt";
    private static final String ASSUAN = "Assuan";
    private static final String CAIRO = "Cairo";
    
    private static final String HUNGARY = "Hungary";
    private static final String BUDAPEST = "Budapest";
    
    private static final String HELLO = "Hello";
    
    private static CityFullCxfRestInterface citySvc;
    private static CountryFullCxfRestInterface countrySvc;
    private static LanguageFullCxfRestInterface languageSvc;
    private static ModifyMultiCxfRestInterface multiSvc;
    private static VisitFullCxfRestInterface visitSvc;
    private static ZipFullCxfRestInterface zipSvc;
    private static TourFullCxfRestInterface tourSvc;
    
    private static Client cityClient;
    private static Client countryClient;
    private static Client languageClient;
    private static Client multiClient;
    private static Client visitClient;
    private static Client zipClient;
    private static Client tourClient;
    
    private static CityFullCxfRestInterface cityProtectedSvc;
    private static Client cityProtectedClient;
    
    private static ProtectedCxfRestInterface highRankProtectedSvc;
    private static Client highRankProtectedClient;
    
    private static ProtectedCxfRestInterface lowRankProtectedSvc;
    private static Client lowRankProtectedClient;
    
    /**
     * BEFORE CLASS: setup proxies, set their media types to APPLICATION_XML. 
     * There seems to be an error in Apache CXF, the REST client seemingly ignores 
     * \@Consumes annotations and always sends text/plain 
     * 
     * The "Accept: *" header seems to be necessary to make a returned string (delete) 
     * be delivered as XML. Omitting it makes delete() fail with the generic 
     * 
     * javax.ws.rs.WebApplicationException 
     *   at com.sun.jersey.server.impl.uri.rules.TerminatingRule.accept(TerminatingRule.java:66)
     * 
     * No idea whose fault this is. I suppose how I treat delete() is wrong (though it works for 
     * soapUI and for Flex clients), otoh I don't see why CXF automatically produces an 
     * "Accept: text/plain" for a return type of String. It could simply use the type from the 
     * interface, and that's "application/xml".
     */
    @BeforeClass
    public static void setupProxies() {

        citySvc = JAXRSClientFactory.create(BASE_URI, CityFullCxfRestInterface.class);
        cityClient = WebClient.client(citySvc);
        cityClient.type(MediaType.APPLICATION_XML);
        cityClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        cityClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        cityClient.header("x-authenticate-userid", "junit");
        
        countrySvc = JAXRSClientFactory.create(BASE_URI, CountryFullCxfRestInterface.class);
        countryClient = WebClient.client(countrySvc);
        countryClient.type(MediaType.APPLICATION_XML);
        countryClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        countryClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        countryClient.header("x-authenticate-userid", "junit");
        
        languageSvc = JAXRSClientFactory.create(BASE_URI, LanguageFullCxfRestInterface.class);
        languageClient = WebClient.client(languageSvc);
        languageClient.type(MediaType.APPLICATION_XML);
        languageClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        languageClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        languageClient.header("x-authenticate-userid", "junit");
        
        multiSvc = JAXRSClientFactory.create(BASE_URI, ModifyMultiCxfRestInterface.class);
        multiClient = WebClient.client(multiSvc);
        multiClient.type(MediaType.APPLICATION_XML);
        multiClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        multiClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        multiClient.header("x-authenticate-userid", "junit");
        
        visitSvc = JAXRSClientFactory.create(BASE_URI, VisitFullCxfRestInterface.class);
        visitClient = WebClient.client(visitSvc);
        visitClient.type(MediaType.APPLICATION_XML);
        visitClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        visitClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        visitClient.header("x-authenticate-userid", "junit");
        
        zipSvc = JAXRSClientFactory.create(BASE_URI, ZipFullCxfRestInterface.class);
        zipClient = WebClient.client(zipSvc);
        zipClient.type(MediaType.APPLICATION_XML);
        zipClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        zipClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        zipClient.header("x-authenticate-userid", "junit");
        
        tourSvc = JAXRSClientFactory.create(BASE_URI, TourFullCxfRestInterface.class);
        tourClient = WebClient.client(tourSvc);
        tourClient.type(MediaType.APPLICATION_XML);
        tourClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        tourClient.header("x-authorize-roles", "azzyzt(200-on-error=false);modify()");
        tourClient.header("x-authenticate-userid", "junit");
        
        cityProtectedSvc = JAXRSClientFactory.create(BASE_URI, CityFullCxfRestInterface.class);
        cityProtectedClient = WebClient.client(cityProtectedSvc);
        cityProtectedClient.type(MediaType.APPLICATION_XML);
        cityProtectedClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        cityProtectedClient.header("x-authorize-roles", "azzyzt(200-on-error=false);");
        cityProtectedClient.header("x-authenticate-userid", "junit");
        
        highRankProtectedSvc = JAXRSClientFactory.create(BASE_URI, ProtectedCxfRestInterface.class);
        highRankProtectedClient = WebClient.client(highRankProtectedSvc);
        highRankProtectedClient.type(MediaType.APPLICATION_XML);
        highRankProtectedClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        highRankProtectedClient.header("x-authorize-roles", "azzyzt(200-on-error=false); admin; senior(rank=3)");
        highRankProtectedClient.header("x-authenticate-userid", "junit");
        
        lowRankProtectedSvc = JAXRSClientFactory.create(BASE_URI, ProtectedCxfRestInterface.class);
        lowRankProtectedClient = WebClient.client(lowRankProtectedSvc);
        lowRankProtectedClient.type(MediaType.APPLICATION_XML);
        lowRankProtectedClient.accept(MediaType.MEDIA_TYPE_WILDCARD);
        lowRankProtectedClient.header("x-authorize-roles", "azzyzt(200-on-error=false); admin; senior(rank=1)");
        lowRankProtectedClient.header("x-authenticate-userid", "junit");
        
    }
    
    /**
     * BEFORE TEST: We dump to System.out, print a line between tests
     */
    @Before
    public void printDelimiter() {
        System.err.println("########################################################\n");
    }
    
    /**
     * TEST: List of all countries
     */
    @Test
    public void testAllCountries() {
        List<Dto> countries = countrySvc.all();
        
        assertNotNull(countries);
        assertTrue(countries.size() >= 3);
        for (Dto d : countries) {
            assertTrue(d instanceof CountryDto);
            CountryDto c = (CountryDto)d;
            dump(c);
        }
    }
    
    /**
     * TEST: City with the ID 1
     */
    @Test
    public void testCityById() {
        CityDto city1 = citySvc.byId(1L);
        
        assertNotNull(city1);
        assertEquals(new Long(1), city1.getId());
        assertEquals("Graz", city1.getName());
        dump(city1);
    }
    
    /**
     * TEST: Sorted list of cities, grouped by ascending country.ID, 
     * names within groups descending
     */
    @Test
    public void testSortedListOfCities() {
        List<Dto> cities = citySvc.list(from("META-INF/xml/sorted_list_of_cities.xml"));
        
        assertNotNull(cities);
        assertTrue(cities.size() >= 12);
        CityDto last = null;
        for (Dto d : cities) {
            assertTrue(d instanceof CityDto);
            CityDto c = (CityDto)d;
            if (last != null) {
                if (c.getCountryId().equals(last.getCountryId())) {
                    // we expect names within country groups to be descending, could be equal
                    assertTrue(c.getName().compareTo(last.getName()) <= 0);
                } else {
                    assertTrue(c.getCountryId() > last.getCountryId());
                }
            }

            last = c;
            dump(c);
        }
    }

    /**
     * TEST: Query with three conditions:
     *  - Country name is "Italy"
     *  - City name does not begin with "r" (case-insensitive)
     *  - City ID is not 8 
     *  - ascending by city name
     */
    @Test
    public void testQueryWithThreeConditions() {
        List<Dto> cities = citySvc.list(from("META-INF/xml/query_with_three_conditions.xml"));
        
        assertNotNull(cities);
        assertTrue(cities.size() >= 2);
        CountryDto co = null;
        CityDto last = null;
        for (Dto d : cities) {
            assertTrue(d instanceof CityDto);
            CityDto c = (CityDto)d;
            
            // is in Italy
            if (co == null) {
                co = countrySvc.byId(c.getCountryId());
                assertEquals("Italy", co.getName());
            } else {
                assertEquals(co.getId(), c.getCountryId());
            }
            
            // does not begin with "r" (case-insensitive)
            char startChar = c.getName().charAt(0);
            assertTrue(startChar != 'r' && startChar != 'R');
            
            // ID is not 8
            assertTrue(c.getId() != 8);
            
            // name ascending
            if (last != null) {
                assertTrue(c.getName().compareTo(last.getName()) >= 0);
            }
            last = c;

            dump(c);
        }
    }

    /**
     * TEST: Query with two betweens:
     *  - City ID is between 2 and 5 
     *  - City name is between "Linz" and "Salzburg" (case-insensitive)
     *  - ascending by city ID
     */
    @Test
    public void testQueryWithTwoBetweens() {
        List<Dto> cities = citySvc.list(from("META-INF/xml/query_with_two_betweens.xml"));
        
        assertNotNull(cities);
        assertTrue(cities.size() >= 7);
        CityDto last = null;
        for (Dto d : cities) {
            assertTrue(d instanceof CityDto);
            CityDto c = (CityDto)d;
            
            // is within range
            Long id = c.getId();
            String name = c.getName();
            assertTrue((id >=2 && id <= 5) 
                    || (name.compareToIgnoreCase(LINZ) >= 0 && name.compareToIgnoreCase(SALZBURG) <= 0));
            
            // id ascending
            if (last != null) {
                assertTrue(c.getId() >= last.getId());
            }
            last = c;

            dump(c);
        }
    }

    /**
     * TEST: Nested expressions
     *  - either in "Italy"
     *  - name starts with "l" (case-insensitive), but is not "Linz"
     */
    @Test
    public void testNestedExpressions() {
        List<Dto> cities = citySvc.list(from("META-INF/xml/nested_expressions.xml"));
        
        assertNotNull(cities);
        assertTrue(cities.size() >= 2);
        CityDto last = null;
        for (Dto d : cities) {
            assertTrue(d instanceof CityDto);
            CityDto c = (CityDto)d;
            
            CountryDto co = countrySvc.byId(c.getCountryId());
            if (!co.getName().equals("Italy")) {
                char startChar = c.getName().charAt(0);
                assertTrue(startChar == 'l' || startChar == 'L');
                assertTrue(c.getId() != 2);
                assertTrue(!c.getName().equals("Linz"));
            }
            
            // name ascending
            if (last != null) {
                assertTrue(c.getName().compareTo(last.getName()) >= 0);
            }
            last = c;

            dump(c);
        }
    }
    
    /**
     * TEST: Store, update and delete a city
     * Store a new city under a wrong name, update its name, finally delete it.
     * This is different from the tutorial, we use Austria in order to not 
     * interfere with the list tests so far. We also do it only once, because
     * we don't test JSON.
     */
    @Test
    public void testStoreUpdateDeleteCity() {
        List<Dto> countries = countrySvc.list(from("META-INF/xml/get_austria.xml"));
        assertNotNull(countries);
        assertEquals(1, countries.size());
        CountryDto austria = (CountryDto)countries.get(0);
        assertEquals("Austria", austria.getName());
        
        CityDto c = new CityDto();
        c.setCountryId(austria.getId());
        c.setName("Villak");
        dump(c);
        
        c = citySvc.store(c);
        assertNotNull(c);
        assertNotNull(c.getId());
        assertTrue(c.getId() > 12);
        assertNotNull(c.getCreateTimestamp());
        assertNotNull(c.getModificationTimestamp());
        assertNotNull(c.getCreateUser());
        assertNotNull(c.getModificationUser());
        assertEquals(c.getCreateTimestamp(), c.getModificationTimestamp());
        assertEquals(c.getCreateUser(), c.getModificationUser());
        dump(c);

        Long id = c.getId();
        Calendar createTst = c.getCreateTimestamp();
        
        c.setName("Villach");
        c = citySvc.store(c);
        assertNotNull(c);
        assertNotNull(c.getId());
        assertEquals(id, c.getId());
        assertNotNull(c.getModificationTimestamp());
        assertTrue(c.getModificationTimestamp().compareTo(createTst) == 1);
        dump(c);
        
        String result = citySvc.delete(id);
        assertEquals("<result>OK</result>", result);
    }
    
    /**
     * TEST: store France and three cities in one call, delete them in one call
     */
    @Test
    public void testStoreDeleteFranceAndCities() {
        List<Dto> dtos = createCountryAndCityDtos(FRANCE, MARSEILLES, PARIS, RENNES);
        dtos = multiSvc.storeMulti(dtos);
        checkCountryAndCityDtos(dtos, FRANCE, MARSEILLES, PARIS, RENNES);
        
        dump(dtos);

        String result = multiSvc.deleteMulti(dtos);
        assertEquals("<result>OK</result>", result);
    }

    /**
     * TEST: store Hungary and a tour through the country, delete them in one call
     * Tests for fix to issue #29
     */
    @Test
    public void testStoreDeleteHungaryAndTour() {
        
        List<Dto> languages = languageSvc.all();
        LanguageDto tourLanguage = (LanguageDto)languages.get(0);
        
        List<Dto> dtos = new ArrayList<Dto>();

        CountryDto hungary = new CountryDto();
        hungary.setId(-1L);
        hungary.setName(HUNGARY);
        dtos.add(hungary);
        
        TourDto tour = new TourDto();
        tour.setCountryId(hungary.getId());
        tour.setLanguageId(tourLanguage.getId());
        dtos.add(tour);
        
        dtos = multiSvc.storeMulti(dtos);
        
        dump(dtos);

        String result = multiSvc.deleteMulti(dtos);
        assertEquals("<result>OK</result>", result);
    }

    /**
     * TEST: store France, replace it with Egypt, make a visit to Luxor 
     * and clean up
     */
    @Test
    public void testStoreFranceReplaceWithEgyptMakeVisit() {
        List<Dto> france = createCountryAndCityDtos(FRANCE, MARSEILLES, PARIS, RENNES);
        france = multiSvc.storeMulti(france);
        checkCountryAndCityDtos(france, FRANCE, MARSEILLES, PARIS, RENNES);
        
        dump(france);

        Long idFrance = ((CountryDto)france.get(0)).getId();

        List<Dto> egypt = createCountryAndCityDtos(EGYPT, ASSUAN, CAIRO);

        StoreDelete sd = new StoreDelete(france, egypt);
        egypt = multiSvc.storeDeleteMulti(sd);
        // check (partially) that France is gone
        boolean lookupFailed = true;
        try {
            countrySvc.byId(idFrance);
            lookupFailed = false;
        } catch (WebApplicationException e) { }
        assertTrue(lookupFailed);
        // now check Egypt
        checkCountryAndCityDtos(egypt, EGYPT, ASSUAN, CAIRO);
        
        dump(egypt);
        
        // create the visit
        Long idEgypt = ((CountryDto)egypt.get(0)).getId();
        
        CityDto luxor = createCity(idEgypt, "Luxor", -1L);
        
        LanguageDto enUK = new LanguageDto();
        enUK.setId("en_UK");
        enUK.setLanguageName("English (UK)");
        
        VisitDto visit = new VisitDto();
        visit.setId(new VisitId(1L, luxor.getId(), enUK.getId()));
        visit.setTotalNumberOfVisitors(5L);
        
        List<Dto> dtos = new ArrayList<Dto>();
        dtos.add(luxor);
        dtos.add(enUK);
        dtos.add(visit);
        dtos = multiSvc.storeMulti(dtos);
        
        assertNotNull(dtos);
        assertEquals(3, dtos.size());
        
        Dto dto = dtos.get(0);
        assertNotNull(dto);
        assertTrue(dto instanceof CityDto);
        CityDto luxorStored = (CityDto)dto;
        assertTrue(luxorStored.getId() > 0);
        
        dto = dtos.get(2);
        assertNotNull(dto);
        assertTrue(dto instanceof VisitDto);
        VisitDto visitStored = (VisitDto)dto;
        assertEquals(luxorStored.getId(), visitStored.getId().getToCity());
        
        dump(dtos);
        
        String result = multiSvc.deleteMulti(dtos);
        assertEquals("<result>OK</result>", result);
        
        result = multiSvc.deleteMulti(egypt);
        assertEquals("<result>OK</result>", result);
    }

    /**
     * TEST: Store a city with the protected client. It does not send a 
     * "modify" credential, thus the server should throw an AccessDenied Exception.
     */
    @Test(expected=ServerWebApplicationException.class)
    public void testTryStoreCity() {
        List<Dto> countries = countrySvc.list(from("META-INF/xml/get_austria.xml"));
        assertNotNull(countries);
        assertEquals(1, countries.size());
        CountryDto austria = (CountryDto)countries.get(0);
        assertEquals("Austria", austria.getName());
        
        CityDto c = new CityDto();
        c.setCountryId(austria.getId());
        c.setName("Villach");
        dump(c);
        
        c = cityProtectedSvc.store(c);
        fail("This should be unreachable");
    }
    
    /**
     * TEST: call a method requiring no rank with low rank
     */
    @Test
    public void testCallProtectedNoWithLowRank() {
        String reply = lowRankProtectedSvc.helloAdmin(HELLO);
        assertNotNull(reply);
        assertEquals(HELLO, reply);
    }
    
    /**
     * TEST: call a method requiring high rank with low rank.
     * The server should throw an AccessDenied Exception.
     */
    @Test(expected=ServerWebApplicationException.class)
    public void testCallProtectedHighWithLowRank() {
        lowRankProtectedSvc.helloSeniorAdmin(HELLO);
        fail("This should be unreachable");
    }
    
    /**
     * TEST: call a method requiring high rank with higher rank
     */
    @Test
    public void testCallProtectedHighWithHighRank() {
        String reply = highRankProtectedSvc.helloSeniorAdmin(HELLO);
        assertNotNull(reply);
        assertEquals(HELLO, reply);
        
    }
    
    /**
     * Helper method that creates DTOs for a country and its cities
     * 
     * @param name of country
     * @param names of cities
     * @return
     */
    private List<Dto> createCountryAndCityDtos(String country, String...cities) {
        List<Dto> result = new ArrayList<Dto>();

        Long idGen = -1L;
        CountryDto co = new CountryDto();
        co.setId(idGen--);
        co.setName(country);
        result.add(co);

        if (cities == null || cities.length == 0) return result;
        
        for (String city : cities) {
            CityDto c = createCity(co.getId(), city, idGen--);
            result.add(c);
        }
        return result;
    }

    /**
     * Helper method to create a city
     * 
     * @param cityName
     * @param countryId
     * @return
     */
    private CityDto createCity(Long countryId, String cityName, Long cityId) {
        CityDto c = new CityDto();
        c.setCountryId(countryId);
        c.setId(cityId);
        c.setName(cityName);
        return c;
    }

    /**
     * Helper method that checks a country and its cities
     * 
     * @param dtos returned from storeMulti() or storeDeleteMulti()
     * @param name of country
     * @param names of cities
     */
    private void checkCountryAndCityDtos(List<Dto> dtos, String country, String...cities) {
        // guard against caller
        assertTrue(cities != null);

        Dto dto;

        // check list
        assertNotNull(dtos);
        assertEquals(1 + cities.length, dtos.size());
        
        dto = dtos.get(0);
        assertNotNull(dto);
        assertTrue(dto instanceof CountryDto);
        
        CountryDto co = (CountryDto)dto;
        Long countryId = co.getId();
        assertNotNull(countryId);
        assertTrue(countryId > 0);
        assertEquals(country, co.getName());
        
        if (cities.length == 0) return;
        
        for (int i = 0; i < cities.length; i++) {
            dto = dtos.get(i + 1);
            assertNotNull(dto);
            assertTrue(dto instanceof CityDto);
            
            CityDto c = (CityDto)dto;
            Long cityId = c.getId();
            assertNotNull(cityId);
            assertTrue(cityId > 0);
            assertEquals(cities[i], c.getName());
            assertNotNull(c.getCountryId());
            assertEquals(countryId, c.getCountryId());
        }
    }

    /**
     * Prints the name of the calling method, followed by an XML dump 
     * of object given as parameter
     * @param o
     */
    private static void dump(Object o) {
        dump(o, 3);
    }
    
    private static void dump(Object o, int depth) {
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        System.err.println(trace[depth].getMethodName()+"():\n");
        JAXB.marshal(o, System.err);
        System.err.println("\n");
    }
    
    private static void dump(List<?> l) {
        for (Object o : l) {
            dump(o, 3);
        }
    }
    
    /**
     * Read content of a file and returns it as a string
     * @param relativeToRootFileName
     * @return
     */
    private String from(String relativeToRootFileName) {
        StringBuilder result = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                getClass().getClassLoader().getResourceAsStream(
                        relativeToRootFileName)));
        String line;
        try {
            while ((line = reader.readLine()) != null) {
                result.append(line);
                result.append('\n');
            }
        } catch (IOException e) {
            throw new Error(e);
        }
        return result.toString().trim();
    }
    
}

Run the test via “Run As / JUnit Test”. You will see some output in the console, and in the JUnit view the bar should stay green and all tests should succeed.

You can repeat the test as often as you like, the tests are written in a way that they clean up after themselves.

I could have made database reinitialization automatically in a @BeforeClass method, but, honestly, automatically dropping a database schema in a unit test suite always make me kind of nervous :)

6.5 Customizing the REST error response

One of the principles of REST is, that a service should behave just like any plain old HTTP server. Thus, if an error occurs, the default behavior of Java REST services is to return “500 Internal server error”, “404 Not Found” or whatever describes the situation most accurately. The rest of the response may contain an “entity” in HTTP lingo, in other words information explaining the type of error.

Unfortunately Adobe Flex clients making a request either get a response or not, and if not, they have no access to anything but the status code. If we want to return descriptive information, we have to use the status “200 OK” and return a special response, for instance an XML element “error”.

That’s exactly what we have implemented, and due to the fact that we have written our own Flex serializer/deserializer anyway, we have no problem reacting to errors appropriately. The drawback is though, that these services not only violate HTTP, they are also incompatible with the Apache CXF REST implementation, because by not properly implementing HTTP, they also don’t properly implement REST. Therefore we need to make error behavior customizable.

So far we have seen customizations that work at generation time (options and cutbacks) or at runtime for all requests (user name translation). Now we need to respond differently for different clients, and in order to do that, a client has to signal the type of response it expects.

Our solution is, by default to be strictly HTTP compliant, unless the client supplies a credential “azzyzt” with a property named “200-on-error”. If so and an error occurs, the response to that request is sent in non-conformant mode. Such a non-conformant response could look more or less like this:

HTTP/1.1 200 OK
Cache-Control: no-store,max-age=0,must-revalidate
Content-Type: application/xml
Content-Length: 142
Date: Wed, 06 Jul 2011 14:16:11 GMT

<error>
<type>FAULT</type>
<code>0</code>
<detail>EntityNotFoundException</detail>
</error>

7 Licenses

 Licensed under the EUPL, Version 1.1 or as soon they
 will be approved by the European Commission - subsequent
 versions of the EUPL (the "Licence");
 You may not use this work except in compliance with the
 Licence.
 
 For convenience a plain text copy of the English version 
 of the Licence can be found in the file LICENCE.txt in
 the top-level directory of this software distribution.
 
 You may obtain a copy of the Licence in any of 22 European
 Languages at:

 http://www.osor.eu/eupl

 Unless required by applicable law or agreed to in
 writing, software distributed under the Licence is
 distributed on an "AS IS" basis,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 express or implied.
 See the Licence for the specific language governing
 permissions and limitations under the Licence.

For the purpose of generating code, Azzyzt JEE Tools make use of and bundles a copy of StringTemplate, which is

 Copyright (c) 2008, Terence Parr
 All rights reserved.
 Redistribution and use in source and binary forms, with or 
 without modification, are permitted provided that the 
 following conditions are met:
 
 Redistributions of source code must retain the above copyright 
 notice, this list of conditions and the following disclaimer.
 
 Redistributions in binary form must reproduce the above copyright 
 notice, this list of conditions and the following disclaimer in 
 the documentation and/or other materials provided with the distribution.
 
 Neither the name of the author nor the names of its contributors 
 may be used to endorse or promote products derived from this software 
 without specific prior written permission.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 POSSIBILITY OF SUCH DAMAGE.

The code generator uses Apache Commons IO which is licensed under the


                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

This documentation was created using Deplate.