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:
6 Customization of the generated code
Copyright (c) 2011, Municipiality of Vienna, Austria Licensed under the EUPL, Version 1.1 or subsequent versions
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
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
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.
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.
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:
doc/cookbook
to create tables and initialize data. If you
ever want to start over, there is also a script to reinitialize the
database.
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.
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:
Ctrl+N
/ Java EE / New Azzyzted JEE Project
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
cookbookEAR
cookbookEJB
cookbookEJBClient
cookbookServlets
“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.
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:
@ManyToMany
associations. It should, but errors in the code
lead to errors in the generated entities. Still, for medium to large
databases, this saves a lot of work. Just insert the
@ManyToMany
associations manually.
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.
Azzyzt introduces the following extra annotations, that can be used on entity fields:
@Internal
@CreateTimestamp
, @ModifyTimestamp
java.text.SimpleDateFormat
@CreateUser
, @ModificationUser
InvocationMetaInfo
being generated at the entry point into
the service, and being passed on via the standard
javax.transaction.TransactionSynchronizationRegistry
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.
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
cookbookEJB
cookbookEJBClient
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.
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
.
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.
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 :)
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.
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 :)
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 :)
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.
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.
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()
byId(id)
id
, for embedded IDs it is a POST request.
list(query_spec)
The non-restricted resources have the same operations as the restricted, and additionally the following:
store(dto)
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)
id
, for embedded IDs it is a POST request.
Additionally there is a resource called
“modifyMulti
”. It has the operations
storeMulti(dtos)
deleteMulti(dtos)
storeDeleteMulti(storeDelete)
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.
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.
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.
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" } ]}
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" }
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.
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
”.
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" } ]}
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" } ]}
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" }
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.
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" }
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"}
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.
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.
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.
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>
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>
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.
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.
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!
Cutting back on features is not the only customization, you can also add features. You do that by adding options.
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:
modify
” is a list containing a single
credential allowing modifications. The credential has no properties
specified
lookup(level=5)
” may allow to lookup in the
application, but the additional property
“level
” could be used to limit the results to a
certain security level. In case of an empty property list, the use of
empty parens (i.e. “lookup()
”) is allowed.
cred_a(p1=v1,p2=v2,p3=v3); cred_b; cred_c(p4=v4); cred_d()
”
is a hypothetic complex example with four credentials, two of them with
properties
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.
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
.
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.
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.
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.
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.
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 :)
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.
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).
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 :)
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>
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.