Serving Clean URLs via Custom Internet Media Types on Tomcat

Guise™ Mummy, Tomcat, and AWS.
https://guise.io/mummy/
Copyright © 2019 GlobalMentor, Inc.

GlobalMentor, Inc.

Full stack software development consulting and training.
Presented by Garret Wilson
Web
http://www.globalmentor.com/
Email
info@globalmentor.com

Filename Extensions and the Web

The original web served static HTML files.
  • /about.html
  • /products/widget.html
Dynamic server frameworks used different filename extensions.
  • /about.jsp
  • /products.jsp?id=widget
Frameworks mapped extensions to template engines.
  1. *.jsp triggers template engine.
  2. Template engine generates HTML.
  3. Browser receives HTTP header with MIME Internet media type:
    Content-Type: text/html

The extension doesn't matter to the browser!

Mapping JSP Engine to *.html

web.xml
…
  <jsp-config>
    <jsp-property-group>
      <url-pattern>*.html</url-pattern>
    </jsp-property-group>
  </jsp-config>
…

If the HTTP Content-Type: text/html header is what matters …

… why do we need filename extensions at all?

“Clean” URLs

  • /about
  • /products/widget
  • Uncluttered.
  • Easier to recognize and remember.
  • Less fragile against implementation changes.
Question
For static content, how does the server know which Content-Type to send?
Answer
Most servers, including Tomcat, have a default mapping based upon the filename extension.
🤦

Apache HTTP Server

Map bare filenames to *.html using mod_rewrite.
…/httpd/conf.d/example.com.conf
<VirtualHost *:80>
  …
  RewriteEngine on
  #if foo requested, return foo.html contents
  RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-d
  RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}\.html -f
  RewriteRule ^(.*)$ $1.html [L]
  #redirect foo.html to foo
  RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-d
  RewriteRule ^(.+)\.html$ $1 [R,L]
</VirtualHost>

Apache Tomcat

  • Apache HTTP Server can be configured with mod_rewrite to proxy requests to Tomcat.
  • Tomcat provides an analogous rewrite valve.

Serving Clean URLs Natively on Tomcat

Why rewrite?
Let Tomcat determine the Internet media type using an alternate approach to filename extension mapping.

Metadata Sidecar Files

A sidecar file sits beside the file to be served.
  • /products/widget
  • /products/widget.@.turf

The .@ sidecar sub-extension is a proposed convention meaning "at" for "attributes".

widget.@.turf

*:
  content-type = |"text/html;charset=UTF-8"|
      *content-MediaType
  title = "Widget"
  keywords = "widget,products,foobar,promotion"
  description = "High quality widget."
;

TURF Format

  • Like JSON with types, dates, comments, ….
  • Like JSON if it were semantic.
  • Like YAML for people who prefer delimiters to indention.
  • Like RDF if it had a pretty serialization.
  • One of the formats of the Uniform Resource Framework (URF).
https://urf.io/turf/

Extending Tomcat to Recognize Metadata Sidecars

Source
io.guise.catalina.webresources
Artifact
io.guise:guise-tomcat

Extend FileResource?

org.apache.catalina.webresources.AbstractResource
…
    @Override
    public final String getMimeType() {
        return mimeType;
    }
…
AbstractResource.getMimeType() is declared final.

Custom WebResourceSet

io.guise.catalina.webresources.SiteDirResourceSet
public class SiteDirResourceSet extends DirResourceSet {
  @Override
  public WebResource getResource(String path) {
    …
    FileResource fileResource = new FileResource(…);
    Path descriptionFile; //determine path to sidecar
    if(exists(descriptionFile)) {
      //load description file; find MIME content type
      fileResource.setMimeType(contentType.toString());
    }
    return fileResource;

Custom WebResourceRoot

io.guise.catalina.webresources.SiteRoot
public class SiteRoot extends StandardRoot {
  @Override
  protected WebResourceSet createMainResourceSet() {
    String docBase = getContext().getDocBase();
    if(docBase != null) {
      File baseFile = new File(docBase);
      if(baseFile.isDirectory()) {
        return new SiteDirResourceSet(…);
      }
    }
    return super.createMainResourceSet();

Install SiteRoot in Tomcat

io.guise.cli.GuiseCli
Tomcat tomcat = new Tomcat();
tomcat.setPort(port);
tomcat.setBaseDir(serverDirectory);
tomcat.getConnector();

Context context = tomcat.addContext("", siteDirectory);
//Metadata sidecar files are in siteDescriptionDirectory.
//(May or may not be the same as siteDirectory.)
context.setResources(new SiteRoot(
    siteDescriptionDirectory));
…

Add Tomcat Default Servlet

io.guise.cli.GuiseCli
Wrapper defaultServlet = context.createWrapper();
defaultServlet.setName("default");
defaultServlet.setServletClass("org.apache.catalina"
    + ".servlets.DefaultServlet");
defaultServlet.setLoadOnStartup(1);
context.addChild(defaultServlet);
context.addServletMappingDecoded("/", "default");
//Add filename extension mappings for non-HTML files.
context.addMimeMapping("png", "image/png");
context.addWelcomeFile("index");
tomcat.start();
How do we remove all those filename extensions?
How do we create all those sidecar files?

Guise™

Internet Application Framework Suite
https://guise.io/
Guise Skeleton
CSS Framework
Guise Mummy
Static Site Generator
Guise Framework
Application Framework

Guise Mummy

Static Site Generator
  • Pure Java.
  • Plays well with Maven.
  • XHTML and Markdown source (more coming).
  • Templates.
  • Automatic nav generation.
  • Serves site with embedded Tomcat.
  • Deploys to AWS S3.

Site Source

  • guise-project.turf
  • src/site/_assets/guise-skeleton.min.css
  • src/site/.template.xhtml
  • src/site/index.md
  • src/site/about.md
  • src/site/products/
  • src/site/products/widget.xhtml

“Mummify” Site

$ guise mummify
   _____       _
  / ____|     (_)
 | |  __ _   _ _ ___  ___
 | | |_ | | | | / __|/ _ \
 | |__| | |_| | \__ \  __/
  \_____|\__,_|_|___/\___|
Guise CLI x.x.x

Mummify...
[INFO] Project directory: …/demo-basic
[INFO] Site source directory: …/demo-basic/src/site
[INFO] Site target directory: …/demo-basic/target/site
[INFO] Site description target directory: …/demo-basic/
    target/site-description
  • target/site/_assets/guise-skeleton.min.css
  • target/site/index.html
  • target/site/about.html
  • target/site/products/index.html
  • target/site/products/widget.html
  • target/site-description/index.html.@.turf

Serve Site with Embedded Tomcat

$ guise serve --browse
…
Serve...
…
[INFO] Server port: 4040
[INFO] Initializing ProtocolHandler ["http-nio-4040"]
[INFO] Starting service [Tomcat]
[INFO] Starting Servlet engine: [Apache Tomcat/9.0.24]
[WARN] Creation of SecureRandom instance for session ID
    generation using [SHA1PRNG] took [598] milliseconds.
[INFO] Starting ProtocolHandler ["http-nio-4040"]
[INFO] Serving site at <http://localhost:4040/>.

Configure Deployment to AWS S3

guise-project.turf
*GuiseProject:
  …

  deploy:
    target = *S3:
      region = ""
      bucket = ""
    ;
  ;

;

Deploy Site

$ guise deploy --browse
…
Deploy...
…
[INFO] Preparing to deploy to AWS region `…` S3 bucket `…`.
[INFO] Creating S3 bucket `…` in AWS region `…`.
[INFO] Setting policy of S3 bucket `…` to public.
[INFO] Configuring S3 bucket `…` for web site access.
[INFO] Deploying to AWS region `…` S3 bucket `…`.
[INFO] Deploying artifact to S3 key `index.html`.
…
[INFO] Successfully deployed site to
    <http://….s3-website.….amazonaws.com/>.

Clean URLs with Guise Mummy, Tomcat, and S3

Configure Generation of Clean URLs

guise-project.turf
*GuiseProject:

  mummy:
    pageNamesBare = true
  ;

  …

;

“Mummify” Site with Clean URLs

$ guise clean # remove previously generated files
$ guise mummify
…
Mummify...
  • target/site/index
  • target/site/about
  • target/site/products/index
  • target/site/products/widget
  • target/site-description/index.@.turf

Serve Site with Clean URLs

$ guise serve --browse
…
Serve...
…
[INFO] Serving site at <http://localhost:4040/>.

Deploy Site with Clean URLs

$ guise deploy --browse
…
Deploy...
…
[INFO] Deploying artifact to S3 key `index`.
[INFO] Deploying artifact to S3 key `about`.
[INFO] Deploying artifact to S3 key `products/index`.
[INFO] Deploying artifact to S3 key `products/widget`.
…
[INFO] Successfully deployed site to
<http://….s3-website.….amazonaws.com/>.

Use Scenarios

  • Use Guise Tomcat extensions in your own embedded Tomcat instance.
  • Incorporate Guise Tomcat extensions into official Tomcat distribution.
  • Serve your site using Guise Mummy with embedded Tomcat.
  • Use Guise Mummy to deploy your site to AWS S3 and to test locally with embedded Tomcat.

This presentation will be shown at ApacheCon Las Vegas, September 10, 2019.

Copyright © 2019 GlobalMentor, Inc.