Realistic legacy-style Tomcat sample: Spring MVC + Hibernate + JNDI PostgreSQL, two WARs
Find a file
anaeem 2946fed92c
Some checks failed
build-image / build (push) Has been cancelled
CI: build with docker (the runner provides docker, not podman)
2026-06-15 15:17:11 +01:00
.gitea/workflows CI: build with docker (the runner provides docker, not podman) 2026-06-15 15:17:11 +01:00
admin Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00
common Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00
db Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00
storefront Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00
.gitignore Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00
Containerfile Add Containerfile + Gitea build-and-push workflow 2026-06-15 15:14:12 +01:00
pom.xml Target Java 11 to match the OpenJDK 11 JBoss Web Server S2I builder 2026-06-14 03:46:35 +01:00
README.md Initial commit: classic-shop legacy Tomcat sample (storefront + admin WARs) 2026-06-11 08:19:24 +01:00

Classic Shop

A deliberately legacy-styled Java web application: a small e-commerce store split into two WARs deployed on a single Tomcat 9, backed by PostgreSQL via a container-managed JNDI datasource. It is a realistic 2015-era enterprise app (Spring MVC 5.3, Hibernate ORM 5.6, JSP views, javax.* Servlet 4 / Java EE 8 APIs, logback file logging, commons-fileupload) that happens to compile and run on Java 17.

It exists as a migration / discovery test fixture: the multi-WAR layout, the JNDI datasource with credentials embedded in context.xml, the host filesystem paths for uploads and logs, and the server-side session cart are all things a VM-to-OpenShift containerisation pass has to notice and externalise.

What it does

  • storefront.war -- customer facing:
    • session-based login against the users table (unsalted SHA-256 hex hashes)
    • product catalog (read side) with list + detail pages
    • a multi-step session cart wizard: add items -> review -> checkout, where checkout writes an orders + order_items row set
    • product image upload via commons-fileupload, saved to a configurable uploads dir (default /var/lib/classic-shop/uploads)
    • a GET /health servlet returning {"status":"UP","db":true|false} with a real SELECT 1 ping against the JNDI datasource
  • admin.war -- back office:
    • product CRUD (create / update / delete)
    • order list + order detail
    • a dashboard with live counts (products / orders / users) from the DB

Module layout

classic-shop/
  pom.xml                     parent (dependency + plugin management)
  common/                     entities, DAOs, services, shared persistence config
    com/example/shop/
      domain/                 User, Product, Order, OrderItem (JPA/Hibernate)
      dao/                    UserDao, ProductDao, OrderDao (Hibernate Session)
      service/                UserService, CatalogService, OrderService (@Transactional)
      util/PasswordUtil       legacy SHA-256 hashing
      PersistenceConfig       JNDI DataSource + Hibernate SessionFactory + tx mgr
  storefront/                 -> storefront.war
    web/                      Login, Catalog, Cart, Upload controllers
    servlet/HealthServlet     /health
    config/StorefrontMvcConfig
    webapp/WEB-INF/views/*.jsp, web.xml, spring/root-context.xml
    webapp/META-INF/context.xml   JNDI datasource (with credentials)
  admin/                      -> admin.war
    web/                      Dashboard, AdminProduct, AdminOrder controllers
    config/AdminMvcConfig
    webapp/WEB-INF/views/*.jsp, web.xml, spring/root-context.xml
    webapp/META-INF/context.xml
  db/init.sql                 idempotent schema + seed data

Both WARs share the common jar and each bootstraps Spring with a classic XML-plus-annotation mix: an XML root-context.xml loaded by ContextLoaderListener simply enables annotation processing and registers the annotated @Configuration classes; the DispatcherServlet uses an AnnotationConfigWebApplicationContext.

Build

Requires Maven and Java 17 (Spring 5.3 / Hibernate 5.6 run fine on 17).

mvn -B -DskipTests package

Produces storefront/target/storefront.war and admin/target/admin.war.

If you only have podman, build in the UBI9 OpenJDK 17 image:

podman run --rm -v "$PWD":/src:Z --user 0 \
  registry.access.redhat.com/ubi9/openjdk-17 \
  bash -c 'cp -r /src /tmp/b && cd /tmp/b && mvn -B -DskipTests package \
           && cp */target/*.war /src/$(dirname)/target/'

Run locally (Tomcat 9 + PostgreSQL)

1. PostgreSQL

createdb classicshop
createuser shopapp
psql -d classicshop -f db/init.sql
psql -d classicshop -c "ALTER USER shopapp WITH PASSWORD 's3cr3t-shop-pw';"
psql -d classicshop -c "GRANT ALL ON ALL TABLES IN SCHEMA public TO shopapp;"
psql -d classicshop -c "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO shopapp;"

2. Tomcat 9

Create the host directories the app writes to:

sudo mkdir -p /var/lib/classic-shop/uploads /var/log/classic-shop
sudo chown -R tomcat:tomcat /var/lib/classic-shop /var/log/classic-shop

The uploads directory is overridable with -Dshop.uploads.dir=... (or the SHOP_UPLOADS_DIR env var); the log directory with SHOP_LOG_DIR.

Drop the PostgreSQL JDBC driver into $CATALINA_HOME/lib/ (e.g. postgresql-42.7.4.jar), copy both WARs into $CATALINA_HOME/webapps/, and start Tomcat. The apps will be available at:

  • storefront: http://localhost:8080/storefront/ (health: /storefront/health)
  • admin: http://localhost:8080/admin/

Each WAR ships its own META-INF/context.xml defining the datasource, so no server-wide config is strictly required. If you prefer a server-wide datasource, put the <Resource> block in $CATALINA_HOME/conf/context.xml instead (see below) and remove it from the WARs.

Datasource snippet (Tomcat conf/context.xml)

This is the JNDI datasource the application looks up at java:comp/env/jdbc/shopDS. The database username and password are embedded in plain text -- this is the classic "secrets baked into the deployment descriptor" pattern, and on a real VM it lives in conf/context.xml:

<Context>
    <Resource name="jdbc/shopDS"
              auth="Container"
              type="javax.sql.DataSource"
              driverClassName="org.postgresql.Driver"
              url="jdbc:postgresql://localhost:5432/classicshop"
              username="shopapp"
              password="s3cr3t-shop-pw"
              maxTotal="20"
              maxIdle="5"
              maxWaitMillis="10000"
              validationQuery="SELECT 1"
              testOnBorrow="true"/>
</Context>

Migration note: a containerisation pass should externalise username, password, and url to environment variables / a Kubernetes Secret rather than shipping them inside the image.

Default credentials

Passwords are stored as unsalted SHA-256 hex digests (legacy scheme; see PasswordUtil).

Username Password Role Notes
admin admin123 ADMIN site administrator
alice password CUSTOMER seeded with sample orders
bob letmein CUSTOMER seeded with sample orders

The storefront uses the customer accounts; the admin WAR has no auth gate of its own in this sample (it is assumed to sit behind a network / reverse-proxy restriction, another migration discovery point).

Seed data

db/init.sql is idempotent (drops and recreates the four tables, then reseeds). It loads:

  • 3 users (1 admin + 2 customers)
  • 12 products (SKU-1001 .. SKU-1012)
  • 5 orders with 12 order-item lines across the two customer accounts, in statuses NEW / PLACED / SHIPPED