A simple web servlet in Jersey

Russell Bateman
August 2020
last update:


Table of Contents

A simple web servlet in Jersey
Toolstack
Steps
pom.xml
Launch IntelliJ IDEA
web.xml
logging.properties
Servlet code
Status/helper code
Servlet-initializer code
IntelliJ IDEA web framework
Run/Debug configuration
Result of HTTP GET
Appendix: project structure
Appendix: Errors you might encounter
    Maven build errors
    Runtime errors
    HTTP status 404 errors
Appendix: Reaching local resources
Appendix: Linking to another page

Servlets are so devilishly nightmarish to get right in IntelliJ IDEA (I found them pretty challenging in Eclipse too). I hope that this one will just work and can be used as a starting point. It almost did, but not quite—why having to play around to get working induced me to lay it out (for you now and for me next time).

This is not a tutorial to explain the theory behind writing servlets, JAX-RS (Jersey) servlets or ReST servlets. Its purpose is to give you what you need to reach the first story of the building of writing servlets. So many tutorials on line advertise getting you up and running, but, in the end, do not succeed. I know because, over the years, I have probably tried all of them and I end up lighting my hair on fire because of HTTP Status 404. I hope instead that this one will work as advertised.

Toolstack

The tool stack we'll use is illustrated by the colorful icons above:

  1. JetBrains' IntelliJ IDEA Ultimate
  2. Jersey, the JAX-RS reference implementation for writing servlets (or ReSTlets) in Java
  3. Apache Tomcat, the definitive web application server container
  4. Google Chrome
  5. GitHub < / > RESTED, a ReST client for the rest of us

Steps

I always set up a project (if, from scratch) this way. Where you put yours is up to you. I put my own, non-commercial ones under ~/dev.

  1. Create a new subdirectory under ~/dev. Today's we'll call melissa-data because I'm thinking of creating a simple service to serve up Melissa Data's address stuff from an installation running on server-grade hardware and I don't want to have to install and run it locally on my development host.

  2. Create the basic filesystem structure for a Java cum web servlet project:
    ~/dev/melissa-data $ mkdir -p src/main/java/com/windofkeltia/servlet
    ~/dev/melissa-data $ mkdir -p src/main/resources
    ~/dev/melissa-data $ mkdir -p src/main/webapp/WEB-INF
    
  3. Populate pom.xml. This file goes in the root of the project. First, here's what I'm using for Maven.
    russ@gondolin ~/dev/melissa-data $ mvn --version
    Apache Maven 3.3.9
    Maven home: /usr/share/maven
    Java version: 11.0.2, vendor: Oracle Corporation
    Java home: /home/russ/dev/jdk-11.0.2
    Default locale: en_US, platform encoding: UTF-8
    OS name: "linux", version: "4.13.0-36-generic", arch: "amd64", family: "unix"
    

    Here's pom.xml. In green, the dependencies that makes this Jersey-based. Please note that you cannot implement this project to run under higher versions of Java language level (see setting for the maven-compiler-plugin below) simply by bumping to, say, Java 11. You will get a status code in your browser at start-up of 500.

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                          http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.windofkeltia.melissa-data</groupId>
      <artifactId>melissa-data</artifactId>
      <version>1.0.0</version>
      <packaging>war</packaging>
    
      <!--
        To build the WAR file, do mvn clean package.
        -->
      <properties>
        <wokprefix>windofkeltia</wokprefix>
        <wokversion>0</wokversion>
        <wokrelease>0</wokrelease>
        <tomcat.version>9.0.7</tomcat.version>
        <jersey2.version>2.27</jersey2.version>
        <jersey-bundle.version>1.19.1</jersey-bundle.version>
        <jaxrs.version>2.0.1</jaxrs.version>
        <servlet-api.version>4.0.0</servlet-api.version>
        <slf4j.version>1.7.7</slf4j.version>
        <log4j.version>1.2.17</log4j.version>
        <junit.version>4.12</junit.version>
        <maven-compiler-plugin.version>3.2</maven-compiler-plugin.version>
        <maven-war-plugin.version>3.2.0</maven-war-plugin.version>
        <maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-bundle</artifactId>
          <version>${jersey-bundle.version}</version>
        </dependency>
        <dependency>
          <groupId>javax.activation</groupId>
          <artifactId>activation</artifactId>
          <version>1.1.1</version>
        </dependency>
        <dependency>
          <groupId>javax.xml.bind</groupId>
          <artifactId>jaxb-api</artifactId>
          <version>2.3.0</version>
        </dependency>
        <dependency>
          <groupId>org.apache.tomcat</groupId>
          <artifactId>tomcat-servlet-api</artifactId>
          <version>${tomcat.version}</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>${servlet-api.version}</version>
        </dependency>
        <!-- Various other dependencies -->
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>${slf4j.version}</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>${slf4j.version}</version>
        </dependency>
        <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>${log4j.version}</version>
        </dependency>
      </dependencies>
    
      <build>
        <finalName>melissa-data</finalName>
        <pluginManagement>
        </pluginManagement>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
            <version>${maven-compiler-plugin.version}</version>
          </plugin>
          <!-- what builds a WAR file -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>${maven-war-plugin.version}</version>
            <configuration>
              <webResources>
                <resource>
                  <!-- this is relative to the same directory as pom.xml -->
                  <directory>src/main/webapp</directory>
                </resource>
              </webResources>
    
              <!-- Magic for maintaining a build timestamp in MANIFEST.MF: -->
              <archive>
                <manifest>
                  <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                  <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                </manifest>
                <manifestEntries>
                  <Build-Time>${maven.build.timestamp}</Build-Time>
                </manifestEntries>
              </archive>
            </configuration>
          </plugin>
          <!-- how to echo out properties and their definitions -->
          <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>1.8</version>
            <executions>
              <execution>
                <phase>compile</phase>
                <configuration>
                  <target>
                    <!-- <echoproperties /> -->
                  </target>
                </configuration>
                <goals>
                  <goal>run</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <configuration>
              <systemPropertyVariables>
                <catalina.home>${project.build.directory}</catalina.home>
                <buildDirectory>${project.build.directory}</buildDirectory>
              </systemPropertyVariables>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
  4. Launch IntelliJ IDEA at some point on this new project by choosing the equivalent of File → Open..., navigating to ~/dev/melissa-data and choosing pom.xml.

  5. Populate src/main/webapps/WEB-INF/web.xml. Java web applications use a deployment descriptor file, web.xml, to establish how URLs map to servlets and other information that informs the web-application server container.
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                                 http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
      <display-name>melissa-data</display-name>
    
      <servlet>
        <servlet-name>melissa-data</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    
        <init-param>
          <param-name>com.sun.jersey.config.property.packages</param-name>
          <param-value>com.windofkeltia.servlet</param-value>
        </init-param>
    
        <init-param>
          <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
          <param-value>true</param-value>
        </init-param>
      </servlet>
    
      <listener>
        <listener-class>com.windofkeltia.servlet.ServletInitializer</listener-class>
      </listener>
    
      <servlet-mapping>
        <servlet-name>melissa-data</servlet-name>
        <url-pattern>/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  6. Next, populate src/main/resources/logging.properties. These serve for now only to ensure you can get help diagnosing start-up error messages like, "One or more listeners failed to start." You'll get a stack trace telling you what's missing, usually a dependency to add to pom.xml.
    org.apache.catalina.core.ContainerBase.[Catalina].level=INFO
    org.apache.catalina.core.ContainerBase.[Catalina].handlers=java.util.logging.ConsoleHandler
    
  7. Let's add source code. First, the servlet code. We pretend to support a simple HTTP GET yielding information about the servlet. That's really all we do here. We do add a POST and you'll thank me for showing that when you roll up your sleeves and begin writing a real servlet based on the clues the POST method here lays down.
    package com.windofkeltia.servlet;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    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.core.Context;
    import javax.ws.rs.core.HttpHeaders;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    @Path( "" )
    public class MelissaDataServlet extends HttpServlet
    {
      private String           message;
      private static final int STATUS_500 = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
    
      @GET
      @Produces( MediaType.TEXT_PLAIN )
      public String getStatusInPlainText( @Context HttpServletRequest request )
      {
        return MelissaDataStatus.getStatusInPlainText( MelissaDataStatus.readManifest( request.getServletContext() ) );
      }
    
      @POST
      @Consumes( { MediaType.TEXT_XML, MediaType.APPLICATION_XML } )
      @Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_XML } )
      public Response postRequest( @Context HttpServletRequest request, @Context HttpHeaders header )
      {
        return null; // ServiceImpl.post( request, header ); TODO: implement this after GET works!
      }
    }
    
  8. Now, some helper code for the servlet, what writes all the "hello world" stuff that comes back when you do a simple HTTP GET. It goes beyond "hello world," of course.
    package com.windofkeltia.servlet;
    
    import javax.servlet.ServletContext;
    import java.io.IOException;
    import java.util.Calendar;
    import java.util.jar.Attributes;
    import java.util.jar.Manifest;
    
    import static java.util.Objects.nonNull;
    
    public class MelissaDataStatus
    {
      private static final String COPYRIGHT  = "Copyright ";
      private static final String COPY_PLAIN = "(c) ";
      private static final String COMPANY    = "by Wind of Keltia, LLC.";
      private static final String RIGHTS     = "Proprietary and confidential. All rights reserved.";
      private static final String PURPOSE    = "Delivers Melissa Data from its server to HTTP clients.";
    
      protected static String getStatusInPlainText( final String MANIFEST )
      {
        StringBuilder sb = new StringBuilder();
        sb.append( "The Melissa Data service is up.\n\n" );
        sb.append( MANIFEST ).append( '\n' );
        sb.append( PURPOSE ).append( "\n\n" );
        sb.append( COPYRIGHT ).append( COPY_PLAIN ).append( getYearRange() ).append( ' ' ).append( COMPANY ).append( '\n' );
        sb.append( RIGHTS ).append( '\n' );
        return sb.toString();
      }
    
      private static String getYearRange()
      {
        final String FIRST_YEAR = "2018";   // just pretending I've been at this for a couple of years
        int          current    = Calendar.getInstance().get( Calendar.YEAR );
        String       YEAR_RANGE = FIRST_YEAR;
    
        if( current > Integer.parseInt( FIRST_YEAR ) )
          YEAR_RANGE += "-" + current;
    
        return YEAR_RANGE;
      }
    
      protected static String readManifest( ServletContext servletContext )
      {
        StringBuilder sb         = new StringBuilder();
        boolean       htmlBreaks = false;
    
        try
        {
          Manifest   manifest   = new Manifest( servletContext.getResourceAsStream( "/META-INF/MANIFEST.MF" ) );
          Attributes attributes = manifest.getMainAttributes();
          String     line;
    
          if( isNotEmpty( line = attributes.getValue( "Manifest-Version" ) ) )
            sb.append( "        Manifest-Version: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Implementation-Title" ) ) )
            sb.append( "    Implementation-Title: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Implementation-Version" ) ) )
            sb.append( "  Implementation-Version: " ).append( line ).append( '\n' );
          // this is something like "Built-By: russ" or buildbot, etc.
          //      if( !isEmpty( line = attributes.getValue( "Built-By" ) ) )
          //        sb.append( "               Built-By: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Specification-Title" ) ) )
            sb.append( "     Specification-Title: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Implementation-Vendor-Id" ) ) )
            sb.append( "Implementation-Vendor-Id: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Build-Time" ) ) )
            sb.append( "              Build-Time: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Created-By" ) ) )
            sb.append( "              Created-By: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Build-Jdk" ) ) )
            sb.append( "               Build-JDK: " ).append( line ).append( '\n' );
          if( isNotEmpty( line = attributes.getValue( "Specification-Version" ) ) )
            sb.append( "   Specification-Version: " ).append( line ).append( '\n' );
    
          return sb.toString();
        }
        catch( IOException ex )
        {
          return "Application build timestamp unavailable";
        }
      }
    
      private static boolean isNotEmpty( final String string ) { return( nonNull( string ) && string.length() > 0 ); }
    }
    
  9. Finally, the servlet initializer:
    package com.windofkeltia.servlet;
    
    import javax.servlet.ServletContext;
    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    
    import org.apache.log4j.Logger;
    
    /**
     * Initialize when the application starts. The way you get this code called
     * is by adding this to web/WEB-INF/web.xml:
     *
     * <pre>
     * <listener>
     *   <listener-class>
     *     com.windofkeltia.servlet.ServletInitializer
     *   </listener-class>
     * </listener>
     * </pre>
     * @author Russell Bateman
     */
    public class ServletInitializer implements ServletContextListener
    {
      private static final Logger log = Logger.getLogger( ServletInitializer.class );
    
      /**
       * This method is called when the servlet context is initialized (when the
       * web application is deployed). You can initialize servlet context-related
       * data here. We use this listener to ensure that we're all up and running
       * before any of the services get registered.
       */
      @Override
      public void contextInitialized( ServletContextEvent event )
      {
        ServletContext     context  = event.getServletContext();
        MelissaDataServlet instance = new MelissaDataServlet();
        final String COPYRIGHT_AND_STATUS = MelissaDataStatus.readManifest( context );
        log.info( "#############################################################" );
        log.info( "Initializing melissa-data application context..." );
        log.info( MelissaDataStatus.getStatusInPlainText( COPYRIGHT_AND_STATUS ) );
      }
    
      /**
       * This method is invoked when the Servlet Context (the web application) is
       * undeployed or the (Tomcat) server shuts down.
       */
      @Override
      public void contextDestroyed( ServletContextEvent event )
      {
        log.info( "Discarded melissa-data application context!" );
      }
    }
    
  10. Add web-development framework support to the project. This requires IntelliJ IDEA Ultimate. It is very hard to develop web applications using the community edition of IDEA. I'm not going to try to explain how to do that. Because I'm a hard-bitten Java developer, I have no problem spending $90 per year to maintain a licensed copy of this IDE. Your mileage may vary, but I'm a true believer.

    In the Project pane, right-click on the project name, melissa-data, and choose Add Framework Support..., then click JAX RESTful Web Services. As far as I can tell, this only gets you com.sun.jersey:jersey-bundle-x.y.z.jar in pom.xml and we've already put that there, but this is how it's supposed to be done.

  11. Now, edit the Run/Debug Configurations. Here are the steps:
    1. Run → Edit Configurations...
    2. Click +.
    3. Scroll down to Tomcat Server, choose local.
    4. Application Server → Configure
      1. Tomcat Home, navigate to private copy of Tomcat server, something like ~/dev/apache-tomcat-9.0.7
      2. Click OK.
    5. URL: http://localhost:8080/melissa-data
    6. On 'Update' action: Restart server
    7. Tomcat Server Settings
      1. HTTP port: 6060
      2. JMX port: 1099
    8. Warning: No artifacts marked for deployment, click Fix
      1. Click +Artifact.
      2. melissa-data:war exploded
    9. Application context: /melissa-data (not /melissa_data_war_exploded—what IDEA puts there)
    10. Click OK.

    In short, make them look like this. I'm using port 6060 because everybody uses port 8080. The port you pick for JMX might conflict too.

  12. Here's the Deployment tab. In IDEA's parlance, what you're making it deploy to Tomcat is an already exploded WAR. Normal deployment of your application to Tomcat would just be the WAR file and Tomcat would explode it.

  13. Launch (run) this service in IDEA by Run → Run... or, Run → Debug.... That's right, you can set breakpoints and see if you hit them or find out why stuff's not happening as you think it should, etc. That's what's so great about running your servlet under IDEA's control. Today, I'm just running. You should see this in the console output window.
    
    /home/russ/dev/apache-tomcat-9.0.7/bin/catalina.sh run
    NOTE: Picked up JDK_JAVA_OPTIONS:  --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
    [2020-08-12 12:31:56,066] Artifact melissa-data:war exploded: Waiting for server connection to start artifact deployment...
    12-Aug-2020 12:31:56.752 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version:        Apache Tomcat/9.0.7
    12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built:          Apr 3 2018 19:53:05 UTC
    12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server number:         9.0.7.0
    12-Aug-2020 12:31:56.754 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name:               Linux
    12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version:            4.13.0-36-generic
    12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture:          amd64
    12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home:             /home/russ/dev/jdk-11.0.2
    12-Aug-2020 12:31:56.755 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version:           11.0.2+9
    ...
    12-Aug-2020 12:31:56.950 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 129 ms
    Connected to server
    [2020-08-12 12:31:57,200] Artifact melissa-data:war exploded: Artifact is being deployed, please wait...
    log4j:WARN No appenders could be found for logger (com.windofkeltia.servlet.ServletInitializer).
    log4j:WARN Please initialize the log4j system properly.
    log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    [2020-08-12 12:31:57,733] Artifact melissa-data:war exploded: Artifact is deployed successfully
    [2020-08-12 12:31:57,733] Artifact melissa-data:war exploded: Deploy took 533 milliseconds
    12-Aug-2020 12:32:06.921 INFO [ContainerBackgroundProcessor[StandardEngine[Catalina]]] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/home/russ/dev/apache-tomcat-9.0.7/webapps/manager]
    12-Aug-2020 12:32:06.951 INFO [ContainerBackgroundProcessor[StandardEngine[Catalina]]] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/russ/dev/apache-tomcat-9.0.7/webapps/manager] has finished in [31] ms
    

    If this goes belly-up before Tomcat even runs your application, you can debug that. Follow the discussion here: Troubleshooting start-up/launch errors to figure out where IntelliJ IDEA puts Tomcat's stuff.

  14. In Chrome (depending on your run-configuration instructions, IDEA should launch some browser for you), for this URL, http://localhost:6060/melissa-data, you should see what's below. You should recognize this as coming from MelissaDataStatus.java. Despite the expression of copyright, feel free to rip the code above off, modify it and use it without remorse. It's why I'm publishing it.
    The Melissa Data service is up.
    
            Manifest-Version: 1.0
      Implementation-Version: 1.0.0
    Implementation-Vendor-Id: com.windofkeltia.melissa-data
                  Build-Time: 2020-08-12T18:29:25Z
                  Created-By: IntelliJ IDEA
                   Build-JDK: 11.0.2
       Specification-Version: 1.0.0
    
    Delivers Melissa Data from its server to HTTP clients.
    
    Copyright (c) 2018-2020 by Wind of Keltia, LLC.
    Proprietary and confidential. All rights reserved.
    
  15. If you prefer using some tool like Postman—I'm using the </> RESTED Client (extension) in Chrome—do that to issue an HTTP GET instead. That's what summoning any page from your browser really is. Again, you should recognize this as coming from MelissaDataStatus.java.

    This </> RESTED Client (or Insomnia—it's insanely great) is what you'll use if you do serious work on a servlet. A browser will easily display anything your servlet does for HTTP GET, however, this is not so for HTTP POST and other verbs.

Appendix: project structure

Of course, I'm leaving out IDEA-created files, read-mes, tests, etc. to make this as simple yet informative as possible.

~/dev $ tree melissa-data
melissa-data
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── windofkeltia
        │           └── servlet
        │               ├── MelissaDataServlet.java
        │               ├── MelissaDataStatus.java
        │               └── ServletInitializer.java
        ├── resources
        │   └── logging.properties
        └── webapp
            └── WEB-INF
                └── web.xml

Appendix: errors you might encounter

Maven build errors

  1. $ mvn clean package
    .
    .
    .
    [ERROR] The goal you specified requires a project to execute but there is no POM in this directory \
        (/home/russ/dev/melissa-data/src/main/java/com/windofkeltia/servlet). Please verify you invoked \
        Maven from the correct directory. -> [Help 1]
    
    

    Did you forget that you must be in the project root to invoke Maven for building the server? Maybe you were down below the project root subdirectory messing about somewhere.

  2. $ mvn clean package
    .
    .
    .
    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-war-plugin:3.2.0:war (default-war) on project \
        melissa-data: Execution default-war of goal org.apache.maven.plugins:maven-war-plugin:3.2.0:war failed: \
        basedir /home/russ/dev/melissa-data/web does not exist -> [Help 1]
    

    The problem is in pom.xml with maven-war-plugin's web-resources configuration. The default usually created is <directory>web</directory> as if you wanted to put web.xml on the path ${PROJECT-ROOT}/web. You need the following path instead if you created web.xml on the path src/main/webapp/WEB-INF/web.xml:

    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <version>${maven-war-plugin.version}</version>
      <configuration>
        <webResources>
          <resource>
            <!-- this is web.xml's parent subdirectory
                relative to pom.xml (project root):
              -->
            <directory>src/main/webapp</directory>
          </resource>
        </webResources>
        .
        .
        .
    

    In the end, it doesn't matter where you put web.xml and IntelliJ IDEA as well as Eclipse web-application projects have waffled over the years as to a preferred location. Whatever the case, Maven's WAR-building plug-in must be able to find it.

Runtime, especially HTTP status code 404 errors

  1. I'm declining to explore various HTTP status errors because to do so goes beyond the purpose of this tutorial. An HTTP 404 is very likely, however, and its solution will lie in closely following the instructions especially as regards

Appendix: Reaching local resources

You want to keep something to distribute with your servlet, let's say some documentation (or something else, but documentation will be our example). In this case, perform a GET on my servlet thus:

GET http://hostname:port/servlet-name/documentation/doc-name

The important tidbits you probably don't know how to do (as you're reading this at all) are highlighted here. Because the documentation itself is kept in HTML/CSS, doing this from a browser yields the documentation page (in the browser—duh).

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

@Path( "/documentation" )
public class Documentation
{
  private static final String BASIC_PATHNAME = "/WEB-INF/classes/documents/doc-name.html";

  @Path( "/doc-name" )
  @GET
  @Produces( MediaType.TEXT_HTML )
  public String getIxmlBasicDocumentationInHtml( @Context HttpServletRequest request ) throws IOException
  {
    // open file on path documents/ixml.html and spew out...
    ServletContext context     = request.getServletContext();
    InputStream    inputStream = context.getResourceAsStream( BASIC_PATHNAME );
    return readFromStream( inputStream );
  }

  private static String readFromStream( InputStream inputStream ) throws IOException
  {
    List< String > lines = readLines( inputStream );

    StringBuilder contents = new StringBuilder();

    for( String line : lines )
      contents.append( line ).append( '\n' );

    return contents.toString();
  }

  private static List< String > readLines( InputStream inputStream ) throws IOException
  {
    List< String > lines = new ArrayList<>();

    try( BufferedReader br = new BufferedReader( new InputStreamReader( inputStream ) ) )
    {
      for( String line = br.readLine(); line != null; line = br.readLine() )
        lines.add( line );
    }

    return lines;
  }
}

Appendix: Linking to another page

Let's use the Melissa Data service as an example. Let's say that, in preparation for your splash page being in HTML and appearing in the browser, you decided to link to some documentation on how to use your service. You are maintaining this documentation in your IDE on the path, src/main/resources/doc/howto.html. It will be deployed as part of your service without you having to do anything special. Users will be able to reach it directly via http://localhost:8080/melissa-data/doc/howto.html.

Let's say your splash page is in HTML (instead of, as in our example much higher above, plain text). Here it is, but we'll put a link inside for your user to click relatively from the splash page without you needing to figure out what the domain and port are. Here's how:

<html>
  <head>
    <title> Melissa Data </title>
  </head>
<body>
  <p> The Melissa Data service is up. </p>

  <pre>
        Manifest-Version: 1.0
  Implementation-Version: 1.0.0
Implementation-Vendor-Id: com.windofkeltia.melissa-data
              Build-Time: 2020-08-12T18:29:25Z
              Created-By: IntelliJ IDEA
               Build-JDK: 11.0.2
   Specification-Version: 1.0.0
  </pre>

  <p> Delivers Melissa Data from its server to HTTP clients. </p>

  <p>
    <a href="/melissa-data/doc/howto">Click here for documentation.</a>
  </p>

  <p>
    Copyright (c) 2018-2020 by Wind of Keltia, LLC.<br />
    Proprietary and confidential. All rights reserved.
  </p>
</body>
</html>

Once running, it will look like this:

The Melissa Data service is up.

        Manifest-Version: 1.0
  Implementation-Version: 1.0.0
Implementation-Vendor-Id: com.windofkeltia.melissa-data
              Build-Time: 2020-08-12T18:29:25Z
              Created-By: IntelliJ IDEA
               Build-JDK: 11.0.2
   Specification-Version: 1.0.0
  

Delivers Melissa Data from its server to HTTP clients.

Click here for documentation.

Copyright (c) 2018-2020 by Wind of Keltia, LLC.
Proprietary and confidential. All rights reserved.

The utility of this note is that it gives you a full example of usage and spares you having to google around to figure out how to do a relative URL in HTML from the page of your content (the page your user is on).