Enterprise Java

Project Student: Webservice Integration

This is part of Project Student. Other posts are Webservice Client With Jersey, Webservice Server with Jersey, Business Layer, Persistence with Spring Data and Sharding Integration Test Data.

Earlier we successfully ran integration tests for both the persistence/business layer (using an embedded H2 database) and the REST server/client layers (using a Jetty server). It’s time to knit everything together.

Fortunately we already have all of our code in place and tested. All we need to do now is create some configuration file magic.
 

Limitations

  • User authentication – no effort has been made to authenticate users.
  • Encryption – no effort has been made to encrypt communications.

Container-Managed Datasource and JNDI

Container-managed datasources have a bad reputation for many developers and I’m not sure why. Confusion with Container-Managed Persistence (CMP), perhaps?

In any case the idea behind a container-managed datasource is simple. You don’t need to figure out how to maintain database connection parameters in a deployed system – no need to modify a deployed webapp (which is insecure) or read a file from the filesystem (which you may not be able to access), etc. You just hand the problem to the person maintaining the webserver/appserver and retrieve the value via JNDI.

Tomcat and Jetty require manual configuration in an XML file. More advanced appservers like JBoss and GlassFish allow you to configure datasources via a nice GUI. It doesn’t really matter since you only have to do it once. Instructions for Tomcat 7 and Jetty.

The key points for Tomcat are that the server library is under $CATALINA_HOME/lib and that might not be where you expect. E.g., in Ubuntu it’s /usr/share/tomcat7/lib, not /var/lib/tomcat7/lib. Second, if the META-INF/context.xml file isn’t picked up from the .war file you must place it under conf/Catalina/localhost/student-ws-webapp.xml (or whatever you have named your .war file). The latter overrides the former so it’s common to set up the .war file to run in the development environment and then override the configuration in the test and production environments.

META-INF/context.xml (Tomcat)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<Context>
 
    <Resource name="jdbc/studentDS"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql:student"
        username="student"
        password="student"
        maxActive="20"
        maxIdle="10"
        maxWait="-1"
        factory="org.apache.commons.dbcp.BasicDataSourceFactory" />
 
</Context>

It’s worth noting that it’s trivial to pass an encryption key via JNDI (as a java.lang.String). This has the same benefits as discussed earlier with regards to the need to modify a deployed webapp or access the server’s filesystem.

(The implementation is a little more complex since you want your actual encryption key to require both the JNDI key AND a filesystem-based salt but this is easy to handle during initial deployment of the webapp.)

JPA Configuration

Our persistence.xml file is extremely minimal. We’ll typically want two persistence units, one for JTA transactions (production) and one for non-JTA transactions (development and testing).

META-INF/persistence.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    version="1.0">
 
    <persistence-unit name="studentPU-local"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <non-jta-data-source>jdbc/studentDS</non-jta-data-source>
    </persistence-unit>
</persistence>

Web Configuration

The web.xml file is nearly identical to the one used in integration testing. The key difference is that it pulls in a resource reference for the container-provided datasource.

WEB-INF/web.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 
    <display-name>Project Student Webservice</display-name>
 
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            com.invariantproperties.sandbox.student.config.PersistenceJpaConfig
            com.invariantproperties.sandbox.student.config.BusinessApplicationContext
            com.invariantproperties.sandbox.student.webservice.config.RestApplicationContext
        </param-value>
    </context-param>
 
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
 
    <servlet>
        <servlet-name>REST dispatcher</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>test</param-value>
        </init-param>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>REST dispatcher</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
 
    <resource-ref>
        <description>Student Datasource</description>
        <res-ref-name>jdbc/studentDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>

Spring Configuration

The Spring configuration for the persistence layer is dramatically different from before because of two changes. Stylistically, we can use a standard configuration file instead of a configuration class since we no longer have to deal with an embedded database.

The more important change is that we’ve moved all of the datasource configuration to the container and we can eliminate it from our spring configuration. We need to point to the right datasource and persistence unit and that’s about it!

applicationContext-dao.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="
 
 
 
 
 
 
 
 
 
 
 
    <context:property-placeholder location="WEB-INF/database.properties" />
 
    <context:annotation-config />
 
    <!-- we use container-based datasource -->
    <jee:jndi-lookup id="dataSource" jndi-name="${persistence.unit.dataSource}"
        expected-type="javax.sql.DataSource" />
 
    <bean name="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="persistenceUnitName" value="${persistence.unit.name}" />
        <property name="packagesToScan" value="${entitymanager.packages.to.scan}" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
    </bean>
 
    <bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" />
 
    <bean name="exceptionTranslation"
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <tx:annotation-driven transaction-manager="transactionManager"
        proxy-target-class="false" />
 
</beans>

Our properties file just contains the name of the datasource, the persistence unit name, and the list of packages to scan that contain JPA annotations.

WEB-INF/database.properties

1
2
3
4
# jpa configuration
entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
persistence.unit.dataSource=java:comp/env/jdbc/studentDS
persistence.unit.name=studentPU-local

Database Schema and Security

Finally the integration tests can use Hibernate auto-creation but we should always explicitly maintain our schemas in a development and production environment. This allows us to maintain infrastructure values in addition to avoiding potential problems from automated tools acting in an unexpected manner.

It’s important to write and test both upgrade and downgrade scripts before going into production. We always need a way to recover gracefully if there’s a problem.

More importantly our database schema should always be owned by a different user than the webapp. E.g., the webapp may use ‘student-user’ for the tables created by ‘student-owner’. This will prevent an attacker using SQL injection from deleting or modifying tables.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
--
-- for security this must run as student-owner, not student-user!
--
 
--
-- create an idempotent stored procedure that creates the initial database schema.
--
create or replace function create_schema_0_0_2() returns void as $$
declare
    schema_version_rec record;
    schema_count int;
begin
    create table if not exists schema_version (
        schema_version varchar(20) not null
    );
 
    select count(*) into schema_count from schema_version;
 
    case schema_count
        when 0 then
            -- we just created table
            insert into schema_version(schema_version) values('0.0.2');
        when 1 then
            -- this is 'create' so we only need to make sure it's current version
            -- normally we accept either current version or immediately prior version.
            select * into strict schema_version_rec from schema_version;
            if schema_version_rec.schema_version  '0.0.2' then
                raise notice 'Unwilling to run updates - check prior version';
                exit;
            end if;     
        else
            raise notice 'Bad database - more than one schema versions defined!';
            exit;
    end case;
 
    -- create tables!
 
    create table if not exists test_run (
        test_run_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        name varchar(80) not null,
        test_date timestamp not null,
        username varchar(40) not null
    );
 
    create table if not exists classroom (
        classroom_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists course (
        course_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists instructor (
        instructor_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );
 
    create table if not exists section (
        section_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists student (
        student_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );
 
    create table if not exists term (
        term_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    -- correction: need to define this!
    create sequence hibernate_sequence;
 
    -- make sure nobody can truncate our tables
    revoke truncate on classroom, course, instructor, section, student, term, test_run from public;
 
    -- grant CRUD privileges to student-user.
    grant select, insert, update, delete on classroom, course, instructor, section, student, term, test_run to student;
 
    grant usage on hibernate_sequence to student;
 
    return;
end;
$$ language plpgsql;
 
-- create database schema
select create_schema_0_0_2() is null;
 
-- clean up
drop function create_schema_0_0_2();

Integration Testing

We can reuse the integration tests from the webservice service but I haven’t copied them to this project since 1) I hate duplicating code and it would be better to pull the integration tests into a separate project used by both server and webapp and 2) it’s easy to configure Jetty to supply JNDI values but for some reason the documented class is throwing an exception and it’s not important enough (at this time) to spend more than a few hours researching.

Source Code

Correction

The schema provided overlooked the ‘hibernate_sequence’ required to create new objects. It must be defined and readable by the ‘student’ user.
 

Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button