Simple Web Application

Russell Bateman
May 2021

Tutuorial implementing Simple Java RESTful Web Service with Jersey—JAX-RS implementation. This proceeds a little differently from this tutorial because IDEA has evolved. I suggest, however, that you bring that tutorial up to follow it as it has illustrations I'm not going to the trouble of reproducing here.

So, if I know all of this already, why am I writing it up in so verbose and naïve a fashion here? This is me trying to leave breadcrumbs for others or, as it turns out, even me to follow if my employer sends me off a different direction (as has happened several times) long enough that I forget the particulars.

My implementation of this tutorial can be found in simple-webapp in my Bitbucket.org Git repository.

Tools

We're going to use:

Steps followed

Create the project...

  1. Launch IntelliJ IDEA, choose File → New Project.
  2. Choose Maven → Create from archetype.
  3. Select org.apache.maven.archetypes:maven-archetype-webapp.
  4. Click Next.
  5. Name: simple-webapp (I'm not a fan of the underscore, so you will see this difference throughout my follow-up of the tutorial).
  6. Location: ~/dev/simple-webapp.
  7. Artifact Coordinates
    1. GroupId: com.madhawa.webapp
    2. ArtifactId: simple-webapp
    3. Version: 1.0-SNAPSHOT
  8. Click Next.
  9. Click Finish.

In the Run pane, you'll see:

.
.
.
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-webapp:RELEASE
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.madhawa.webapp
[INFO] Parameter: artifactId, Value: simple-webapp
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.madhawa.webapp
[INFO] Parameter: packageInPathFormat, Value: com/madhawa/webapp
[INFO] Parameter: package, Value: com.madhawa.webapp
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: com.madhawa.webapp
[INFO] Parameter: artifactId, Value: simple-webapp
[INFO] Project created from Archetype in dir: /tmp/archetypetmp/simple-webapp
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.666 s
[INFO] Finished at: 2021-05-07T11:34:42-06:00
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0

Set up the project...

  1. Expand src in the Project pane.
  2. Right-click main and the java subdirectory.
  3. Right-click the new directory and Mark directory as... Sources Root.
  4. Right-click java and choose New → Package.
  5. Name the package com.madhawa.services and press Enter.
  6. Create a new class by right-clicking the new package, choosing New → Java Class, naming the class HelloService and pressing Enter. (Don't put any code into this new Java class as yet.)
  7. Right-click and erase file index.jsp (or comment out the insides) as this tutorial is not going to cover JavaServer Pages (JSP.

Develop the ReST service...

This service will simply print (to a page in your chosen browser), "Hi <your client name>". This will set up the most important configuration file of any web application, WEB-INF/web.xml. The paragraphs of the original article discussing this are very much worth reading.

    web.xml
  1. Expand WEB-INF to reveal web.xml.
  2. Open web.xml.
  3. Fill this file with the following:
    <!DOCTYPE web-app PUBLIC
          "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
          "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
    <web-app>
      <display-name>simple-webapp</display-name>
    
      <servlet>
        <servlet-name>jersey-servlet</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.madhawa.services</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
    
      <servlet-mapping>
        <servlet-name>jersey-servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  4. You will notice that the <servlet-name>, jersey-serlvet is misspelled. Feel free to correct this (as I have here).

  5. pom.xml
  6. You'll see some red ink in web.xml. This means there are JARs to add to the project. Do this next. Under <dependencies> in your Maven pom.xml file, add the following. The first two are the JAX-RS reference implementation (Jersey) server and a client for this server. The last one contains the JAX-RS APIs (annotations, etc.) for use in writing Java code. Now, I first respected the addition of the Jersey client, but, in fact, the client's never used. Therefore, I took out (and the tutorial still works) the highlighted paragraph below.
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-servlet</artifactId>
      <version>1.19.4</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.19.4</version>
    </dependency>
    <dependency>
      <groupId>javax.ws.rs</groupId>
      <artifactId>javax.ws.rs-api</artifactId>
      <version>2.1.1</version>
    </dependency>
    
    My goal is to make this whole thing as small as possible—only the bare minimum. I did leave in the dependency for JUnit testing in case I want to come back and demonstrate unit tests.

  7. HelloService.java
  8. Open the HelloService.java you added in an earlier step and fill it out with the following. As for web.xml, the tutorial's discussion is very useful to read. (The tutorial calls this class a POJO. However, in practical usage, it isn't. In my mind, a POJO is more synonymous with bean—a POJO is the implementation thereof. Otherwise, all Java classes would be POJOs which leads to this term being useless.)
    package com.madhawa.services;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.PathParam;
    import javax.ws.rs.Produces;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    @Path( "/hello" )
    public class HelloService
    {
      @GET
      @Path( "{clientName}" )
      @Produces( MediaType.TEXT_PLAIN )
      public Response greetClient( @PathParam( "clientName" ) String name )
      {
        String output = "Hi " + name;
        return Response.status( 200 ).entity( output ).build();
      }
    }
    

Set up Tomcat

Apache Tomcat is a web application container, basically, an application that runs Java-based applications acting as servers, specifically:

The part of Tomcat that is specifically the servlet contain, and that we're using in here, was originally something separate named Catalina.

Apache Tomcat is separate software from IntelliJ IDEA. IntelliJ IDEA, Eclipse and all other Java IDEs integrate tightly with it. In the case of IntelliJ IDEA, this is considered a professional-level integration and comes only in the paid level product, Ultimate, and not the Community Edition. As someone with 10 years experience using Eclipse for much software including web services and now 10 years using IntelliJ IDEA, I can testify that the sophistication, technical support and cleaner ease of use of the latter is well worth the price (nominally just under $100 per annum) if you're a professional Java software developer.

  1. If you do not have Tomcat on your development platform (host computer), then install precisely Apache Tomcat 9 from here. Do not attempt to use the later version 10 as it is not integrated with the IDE. As I use Linux, I simply download the tarball (tar.gz) to /home/russ/dev and explode it in place thus
    russ@tirion ~/dev $ tar -zxf apache-tomcat-9.0.45.tar.gz
    
  2. Tell IntelliJ IDEA where it is:
    1. Go to File → Settings... (Ctrl-Alt-S).
    2. Click Build, Execution, Deployment.
    3. Click Application Servers.
    4. Click the + sign in the middle pane, then click Tomcat Server.
    5. Navigate from Tomcat Home: to the root of the copy of Tomcat you just exploded and click OK.
    6. Click OK again. You should see the result added to the list of application servers IDEA knows about.
  3. Now that you have Apache Tomcat installed, you can create a Run/Debug Configuration that uses it:
    1. Go to Run → Edit Configurations....
    2. Click the + sign in the left pane.
    3. Look for Tomcat Server → Local and click it.
    4. Unless you're already using ports 8080 and 1099 for something else, you can leave all that configuration as-is.
    5. Pick your browser under Open browser; it's already set up to use your computer's configured default browser.
    6. At the bottom you should see a warning, Warning: No artifacts marked for deployment and a button, Fix. Click this button which will take you to the Deployment tab.
    7. A pop-up, Select and artifact to deploy gives you the choice between simple-webapp:war or simple-webapp:war exploded. Choose the first one for now.
    8. The resulting Application context: need basically to hold what's in <display-name> inside web.xml. What's in URL: is basically what you want to happen when Tomcat is launched. Thereafter, you can change the URL (to whatever is legal and supported by your application) to what you want and see the effects immediately (it's a working application).

    You should have effectuated the following Run/Debug Configurations. Click to see them full size.

  4. Now a new pane across the bottom of IDEA should have opened with a not-yet-started Tomcat server in it. Do one of...
    • Run → 'Tomcat 9.0.45'
    • Right-click on simple-webapp:war (under Services).
    • Click any green right-facing triangle you might see anywhere in IDEA.

At the end of IDEA's Services Output pane, you should see:


...
07-May-2021 16:27:18.170 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
07-May-2021 16:27:18.176 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [22] milliseconds
Connected to server
[2021-05-07 04:27:18,653] Artifact simple-webapp:war: Artifact is being deployed, please wait...
07-May-2021 16:27:18.954 INFO [RMI TCP Connection(4)-127.0.0.1] org.apache.jasper.servlet.TldScanner.scanJars At least ...
07-May-2021 16:27:18.970 INFO [RMI TCP Connection(4)-127.0.0.1] com.sun.jersey.api.core.PackagesResourceConfig.init Sca...
  com.madhawa.services
07-May-2021 16:27:18.984 INFO [RMI TCP Connection(4)-127.0.0.1] com.sun.jersey.api.core.ScanningResourceConfig.logClass...
  class com.madhawa.services.HelloService
07-May-2021 16:27:18.984 INFO [RMI TCP Connection(4)-127.0.0.1] com.sun.jersey.api.core.ScanningResourceConfig.init No ...
07-May-2021 16:27:19.025 INFO [RMI TCP Connection(4)-127.0.0.1] com.sun.jersey.server.impl.application.WebApplicationIm...
[2021-05-07 04:27:19,264] Artifact simple-webapp:war: Artifact is deployed successfully
[2021-05-07 01:44:36,613] Artifact simple-webapp:war: Deploy took 457 milliseconds
07-May-2021 16:27:28.171 INFO [Catalina-utility-2] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web...
07-May-2021 16:27:28.185 INFO [Catalina-utility-2] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of...

Troubleshooting start-up/launch errors

You might see something like:

29-Oct-2020 14:29:15.056 SEVERE [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.core.StandardContext.startInternal One or more listeners failed to start.
    Full details will be found in the appropriate container log file
29-Oct-2020 14:29:15.060 SEVERE [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.core.StandardContext.startInternal Context [/mdht-restlet] startup failed due to previous errors

...and you wonder where the "approrpriate container log file" is. In the launch of Tomcat, in the Server tab of the Services pane, you'll see Output containing lines like:

29-Oct-2020 14:29:13.735 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name:   Apache Tomcat/9.0.45
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server built:          Mar 30 2020 10:29:04 UTC
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version number: 9.0.45.0
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Name:               Linux
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log OS Version:            5.4.0-89-generic
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Architecture:          amd64
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Java Home:             /home/russ/dev/jdk-11.0.10+9
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version:           11.0.10+9
29-Oct-2020 14:29:13.737 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Vendor:            AdoptOpenJDK
29-Oct-2020 14:29:13.738 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_BASE:         /home/russ/.cache/JetBrains/IntelliJIdea2020.1/tomcat/be59cf09-7bda-42fa-9b84-97966e2e7a88
29-Oct-2020 14:29:13.738 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log CATALINA_HOME:         /home/russ/dev/apache-tomcat-9.0.45

The path in the bold line above shows you where IntelliJ IDEA put your files. There you'll see:

drwxrwxr-x  5 russ russ 4096 Oct 29 14:29 .
drwxrwxr-x 11 russ russ 4096 Oct 29 14:29 ..
drwxrwxr-x  3 russ russ 4096 Oct 29 14:29 conf
-rw-------  1 russ russ   21 Oct 29 14:29 jmxremote.access
-rw-------  1 russ russ  479 Oct 29 14:29 jmxremote.password
drwxrwxr-x  2 russ russ 4096 Oct 29 14:29 logs
drwxr-x---  3 russ russ 4096 Oct 29 14:29 work

...and, under logs, you'll see:

drwxrwxr-x 2 russ russ 4096 Oct 29 14:29 .
drwxrwxr-x 5 russ russ 4096 Oct 29 14:29 ..
-rw-r----- 1 russ russ 7692 Oct 29 14:29 catalina.2020-10-29.log
-rw-r----- 1 russ russ    0 Oct 29 14:29 host-manager.2020-10-29.log
-rw-r----- 1 russ russ 4687 Oct 29 14:29 localhost.2020-10-29.log
-rw-r----- 1 russ russ    0 Oct 29 14:29 localhost_access_log.2020-10-29.txt
-rw-r----- 1 russ russ    0 Oct 29 14:29 manager.2020-10-29.log

The original message that sent you looking for the problem is in catalina.out while, likely, the clues to the exact cause is in localhost.2020-10-29.log ("Start Exception sending context initialized event to listener instance of class"). Inspect the indicated initializer for any potential exception that could be thrown. There is probably a stack trace for the exception beginning the next line in that file.

Troubleshooting the inevitable HTTP Status 404

However, the new page launched in your browser (http://localhost:8080/simple_webapp_war/) will appear thus if you failed to get the Run/Debug Configuration just right:

HTTP Status 404 – Not Found

Type Status Report Message The requested resource [/simple_webapp_war/] is not available Description The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.
Apache Tomcat/9.0.45

...welcome to the club: your woes have only just begun. It probably means that you have not set up the Run/Debug Configuration just right.

According to the following places in the code and configuration, as well as to the tutorial itself, the address line in your browser should be:

http://localhost:8080/simple-webapp/rest/hello/madhawa
-----  -------------- ------------- ---- ----- -------
\                                 /    ↑     ↑       ↑
 ---------------------------------     |     |       |
                ↑                      |     |       +----HelloService.java greetClient( name ) method parameter
                |                      |     +------------HelloService.java @Path( "/hello" ) annotation
                |                      +------------------web.xml <url-pattern>
                +-----------------------------------------Run/Debug Configuration→Server→URL

The secret is to regard Run/Debug Configuration as magic: you must get it right. My illustrating images above show what to put in. The original tutorial was less than clear about this.

Follow-up...

...and yet, who wants the rather redundant and useless path rest/ imposed in the URL? I removed this from the Run/Debug Configuration plus web.xml:

<servlet-mapping>
  <servlet-name>jersey-servlet</servlet-name>
  <url-pattern>/*</url-pattern>
</servlet-mapping>

Important notes...

...that came out of the original discussion in the tutorial. Below you see some annotations, or keywords prepended with an ærobase (@).

  • web.xml stands in the web service as configuration that reveals how the service will work. For example, it binds any and all services (we'll have only one) in the package, com.madhawa.services, to Jersey by way of the magic parameter name, com.sun.jersey.config.property.packages.

  • In web.xml is further defined...
    1. a servlet-name, which must exactly match in both paragraphs servlet and servlet-mapping in order to bind the two definitions together (in complex web.xml files, there can be multiple servlet descriptions).
    2. a url-pattern (in servlet-mapping) which can conflict with @Path in a service class and become a source of woes (leading to the ubiquitous bane, HTTP Status 404). Conceptually, this is a template for a path element added between the hostname/port number and the @Path in a service class.

  • @Path defines the resource base URI for the service whose class it annotates. This path extends the hostname in the URL.

  • @GET marks the method called in response to the HTTP GET method.

  • @Produces defines how the method called will respond.

  • However, the most important thing to learn is that getting the real code bits of the application right (web.xml and your service code in HelloService.java) are no match for botching IDEA's Run/Debug Configuration dialog; get this wrong and you'll never be able to run your application (though likely it will work in Tomcat after you drop the WAR file into /opt/tomcat/webapps).

HTTP status code 404: it's the very devil!

This status code is the most annoying and hardest to debug. Things to look for as you try to tie it down:

  • In web.xml there is a parameter
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>path to your service code</param-value>
    </init-param>
    
  • Does your application name contain a hyphen? The correct name must be reflected in the name in servlet-mapping:
    <servlet-mapping>
      <servlet-name>ccd-validator</servlet-name>
      <url-pattern>/*</url-pattern>
    </servlet-mapping>
    
  • On using a hyphen, IntelliJ IDEA loves to ignore you and substitute an underscore. Look through the Run/Debug Configuration for an unintended underscore in...
    • Under the Server tab, Open browser → URL:
    • Under the Deployment tab, ...
      • Deploy at the server startup (list)
      • Application context:
  • Are all the port numbers expected in synch?
    • Under the Server tab, Open browser → URL:
    • Under the Server tab, Tomcat Server Settings...
      • HTTP port:
      • HTTPs port:
      • JMX port:

HTTP status code 406: it's the Accept: mismatch

If you're getting 406, you might examine

  1. the Accept: parameter (in Postman, in curl or the HTTP client you're using),
  2. what you've specified in your servlet code; for example, are you specifying "text/html" for Accept: (instead of "text/plain") expecting it to reach this method which you coded to cough up HTML in the reply?
    @GET
    @Produces( MediaType.TEXT_PLAIN )
    public String getStatusInPlainText( @Context HttpServletRequest request )
    {
      return ...
    }
    
  3. If using curl, a mistake in the Accept: parameter often results in no errors at all and you will wonder if it reached the servlet at all. For example, a simple, medullary spelling mistake substituting "test" for "text":
    $ curl http://localhost:8080/simple-servlet/ -H 'Accept: test/plain'
    
  4. Etc.

More notes: adding a servlet initializer (listener)

Let's add some code that will display elements of your MANIFEST.MF in case someone hits your servlet simply (http://localhost:8080/simple-webapp/) in the browser (an HTTP GET method):

ServletInitializer.java:
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ServletInitializer implements ServletContextListener
{
  private static final Logger log = LoggerFactory.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();
    try
    {
      ;
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
    final String COPYRIGHT_AND_STATUS = SimpleWebAppStatus.readManifest( context );
    log.info( "#############################################################" );
    log.info( "Initializing simple-webapp context..." );
    log.info( SimpleWebAppStatus.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 simple-webapp context!" );
  }
}
SimpleWebAppStatus.java:
package com.etretatlogiciels.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;

@SuppressWarnings( "StringBufferReplaceableByString" )
public class SimpleWebAppStatus
{
  private static final String COPYRIGHT  = "Copyright ";
  private static final String COPY_PLAIN = "(c) ";
  private static final String COMPANY    = "by Etretat Logiciels, LLC.";
  private static final String RIGHTS     = "Proprietary and confidential. All rights reserved.";
  private static final String PURPOSE    = "A simple web application.";

  protected static String getStatusInPlainText( final String MANIFEST )
  {
    StringBuilder sb = new StringBuilder();
    sb.append( "Simple webapp 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";
    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' );
      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 );
  }
}

To get something like this to work, you'll have to amend the web-deployment descriptor (web.xml) by adding this to it (to <web-app>):

  
    com.etretatlogiciels.servlet.ServletInitializer
  

Appendix: HTTP Status 500 error

Now, when you launch Tomcat from IDEA, you see a 500 error and stuff like this (I shortened it):

HTTP Status 500—Internal Server Error

Type Exception Report Message Servlet.init() for servlet [simple-webapp] threw exception Description The server encountered an unexpected condition that prevented it from fulfilling the request. Exception javax.servlet.ServletException: Servlet.init() for servlet [simple-webapp] threw exception org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) java.base/java.lang.Thread.run(Thread.java:834) Root Cause java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present java.base/sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:117) java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) java.base/sun.reflect.generics.visitor.Reifier.reifyTypeArguments(Reifier.java:68) java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:138) java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) java.base/sun.reflect.generics.repository.ClassRepository.computeSuperInterfaces(ClassRepository.java:117) java.base/sun.reflect.generics.repository.ClassRepository.getSuperInterfaces(ClassRepository.java:95) java.base/java.lang.Class.getGenericInterfaces(Class.java:1137) com.sun.jersey.core.reflection.ReflectionHelper.getClass(ReflectionHelper.java:629) com.sun.jersey.core.reflection.ReflectionHelper.getClass(ReflectionHelper.java:625) com.sun.jersey.core.spi.factory.ContextResolverFactory.getParameterizedType(ContextResolverFactory.java:202) ... Root Cause java.lang.ClassNotFoundException: javax.xml.bind.JAXBContext org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1364) org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1187) java.base/java.lang.Class.forName0(Native Method) java.base/java.lang.Class.forName(Class.java:398) java.base/sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:114) java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) java.base/sun.reflect.generics.visitor.Reifier.reifyTypeArguments(Reifier.java:68) java.base/sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:138) java.base/sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) java.base/sun.reflect.generics.repository.ClassRepository.computeSuperInterfaces(ClassRepository.java:117) java.base/sun.reflect.generics.repository.ClassRepository.getSuperInterfaces(ClassRepository.java:95) java.base/java.lang.Class.getGenericInterfaces(Class.java:1137) com.sun.jersey.core.reflection.ReflectionHelper.getClass(ReflectionHelper.java:629) com.sun.jersey.core.reflection.ReflectionHelper.getClass(ReflectionHelper.java:625) com.sun.jersey.core.spi.factory.ContextResolverFactory.getParameterizedType(ContextResolverFactory.java:202) ...

What's happening is that you're likely using a later version of the JDK than you had it working under (or, maybe you never had this functionality working), such as JDK 11. JDK 11 removed support for java.xml.bind, even if, in your project, you've told it to work at the Java 1.8 level. This used to define the Java Architecture for XML Binding (JAXB) API. The solution is to add it by hand (to pom.xml):

<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-core</artifactId>
  <version>2.3.0.1</version>
</dependency>
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.3.1</version>
</dependency>
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>2.3.1</version>
</dependency>

You must have all three or you'll get stack traces with java.lang.NoClassDefFoundError even assuming that you still get the desired effect (instead of HTTP Status 500).

With the code above working, you should see something like this in the browser page that pops up:

Simple web-app is up.

        Manifest-Version: 1.0
    Implementation-Title: simple-webapp
  Implementation-Version: 1.0.0
     Specification-Title: simple-webapp
Implementation-Vendor-Id: com.etretatlogiciels.simple-webapp
              Build-Time: 2021-05-13T23:16:45Z
              Created-By: IntelliJ IDEA
               Build-JDK: version 11.0.10
   Specification-Version: 1.0.0

Delivers simple-webapp support.

Copyright (c) 2021 by Etretat Logiciels, LLC.
Proprietary and confidential. All rights reserved.

...and, if you look in IDEA's bottom pane in the output from Tomcat, you'll see something like:

...
Connected to server
[2021-05-13 05:17:06,861] Artifact simple-webapp:war: Artifact is being deployed, please wait...

[RMI TCP Connection(2)-127.0.0.1] INFO com.etretatlogiciels.servlet.ServletInitializer - #############################################################
[RMI TCP Connection(2)-127.0.0.1] INFO com.etretatlogiciels.servlet.ServletInitializer - Initializing simple-webapp context...
[RMI TCP Connection(2)-127.0.0.1] INFO com.etretatlogiciels.servlet.ServletInitializer - simple-webapp is up.

        Manifest-Version: 1.0
    Implementation-Title: simple-webapp
  Implementation-Version: 1.0.0
     Specification-Title: simple-webapp
Implementation-Vendor-Id: com.etretatlogiciels.simple-webapp
              Build-Time: 2021-05-13T23:16:45Z
              Created-By: IntelliJ IDEA
               Build-JDK: version 11.0.10
   Specification-Version: 1.0.0

Delivers servlet support to HTTP clients.

Copyright (c) 2021 by Etretat Logiciels, LLC.
Proprietary and confidential. All rights reserved.
...

Appendix: HTTP Status 500, conflicting URI templates


11-Mar-2022 12:09:41.381 INFO [http-nio-3030-exec-1] com.sun.jersey.api.core.ScanningResourceConfig.init No provider classes found.
11-Mar-2022 12:09:41.425 INFO [http-nio-3030-exec-1] com.sun.jersey.server.impl.application.WebApplicationImpl._initiate Initiating \
    Jersey application, version 'Jersey: 1.19.4 05/24/2017 03:46 PM'
11-Mar-2022 12:09:41.767 SEVERE [http-nio-3030-exec-1] com.sun.jersey.spi.inject.Errors.processErrorMessages The following errors and \
    warnings have been detected with resource and/or provider classes:
  SEVERE: Conflicting URI templates. The URI template / for root resource class com.windofkelia.services.ValidationService and the \
    URI template / transform to the same regular expression (/.*)?

This problem occurs when you've a bit randomly created (probably muliple) service classes and given @Path, @GET, @POST, @Consumes, @Produces, etc. that are illogical, duplicates of themselves, etc.

The servlet must be able to determine, based on the in-coming URL and also the Content-Type the calling applications is sending (if there's a payload) and what it will Accept back in the reply.

For example, let's imagine the following services in our servlet. We can toss out SampleService.java as being part of the problem since its @Path( "/sample ) puts it out of the running with the other two.

SampleService.java:

This service expects "/sample" in the URL and different Accept header parameters telling it which of HTML or plain text to reply with. There's no conflict here between the path with the other two services nor between this service's GET handlers because the Accept is different.


@Path( "/sample" )
public class SampleService
{
  @GET
  @Produces( MediaType.TEXT_HTML )
  public Response validateSampleAsHtml( @Context HttpServletRequest request )

  @GET
  @Produces( MediaType.TEXT_PLAIN )
  public Response validateSampleAsPlainText( @Context HttpServletRequest request )
}
Status.java:

In this service, all HTTP GET requests on the same URL, but the replies requested are HTML, plain text, XML or JSON—no conflict internally here.

@Path( "" )
public class Status
{
  @GET
  @Produces( MediaType.TEXT_HTML )
  public String getStatusInHtml( @Context HttpServletRequest request )

  @GET
  @Produces( MediaType.TEXT_PLAIN )
  public String getStatusInPlainText( @Context HttpServletRequest request )

  @GET
  @Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_XML } )
  public String getStatusInXml( @Context HttpServletRequest request )

  @GET
  @Produces( MediaType.APPLICATION_JSON )
  public String getStatusInJson( @Context HttpServletRequest request )
}
ValidationService.java:

In this service, all HTTP POST requests on the same URL, but the replies requested are either HTML or plain text—no conflict internally here.

@Path( "" )
public class ValidationService
{
  @POST
  @Consumes( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON } )
  @Produces( MediaType.TEXT_HTML )
  public Response validateToHtml( @Context HttpServletRequest request, String payload )

  @POST
  @Consumes( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON } )
  @Produces( MediaType.TEXT_PLAIN )
  public Response validateToPlainText( @Context HttpServletRequest request, String payload )
}

For the last two services, however, maybe there's a conflict between the URL, the HTTP verb or the type of reply demanded. We know

  1. the URL is identical between all 6 requests,
  2. the four Status.java methods don't expect a payload (Content-Type),
  3. the two ValidationService.java methods expect a payload, so they don't possibly conflict with Status.java.
  4. If you still can't get satisfaction (by visual inspection), you can try "disabling" one of your services to see if your servlet starts up. If it does start up, then the commented-out service contains a conflicting method as currently configured.

    This is quickly done by simply commenting out the @Path above the service's classname in Java.
  5. It's possible to comment out various methods to eliminate them from contention too.
  6. My example here is more for illustration and less genuinely conclusive.

Appendix: HTTP Status 500, Media-type conflict

You see something like this not in the page that pops up, but in the server log (inside IntelliJ IDEA's Services window).


...
01-Jun-2021 11:33:39.766 SEVERE [http-nio-8089-exec-3] com.sun.jersey.spi.inject.Errors.processErrorMessages \
    The following errors and warnings have been detected with resource and/or provider classes:
  SEVERE: Producing media type conflict. The resource methods \
    public java.lang.String com.windofkeltia.services.CapabilitiesService.getCapabilityInXml(javax.servlet.http.HttpServletRequest,java.lang.String)
    throws java.io.IOException \
  and public java.lang.String com.windofkeltia.services.CapabilitiesService.getFullCapabilityInXml(javax.servlet.http.HttpServletRequest) \
    throws java.io.IOException can produce the same media type
...

What this means is that you have messed up coding (in this case) CapabilitiesService.java. You have two, annotated methods you're telling Jersey about that return exactly the same thing under the same calling circumstances. Therefore, Jersey will not be able to decide which method to call and therefore would be broken, Jersey knows this and it's telling you about it.

The solution is to compare both the signatures of the methods as well as the annotations—thinking about the two (or more) until it dawns on you how they're indistinguishable and decide what to do about it.

You can minimize the perplexity of finding this by taking care never to add more than one method at a time before launching your web application to vet its acceptability to Jersey..

Appendix: Simple web application blanks

pom.xml:
<?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.servlet</groupId>
  <artifactId>servlet</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>servlet</name>
  <description>Simple Java ReSTful web service with JAX-RS Jersey</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-servlet</artifactId>
      <version>1.19.4</version>
    </dependency>
    <dependency>
      <groupId>javax.ws.rs</groupId>
      <artifactId>javax.ws.rs-api</artifactId>
      <version>2.1.1</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.0</version>
    </dependency>

    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-core</artifactId>
      <version>2.3.0.1</version>
    </dependency>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>com.sun.xml.bind</groupId>
      <artifactId>jaxb-impl</artifactId>
      <version>2.3.1</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>servlet</finalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>
web.xml:
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
  <display-name>servlet</display-name>

  <servlet>
    <servlet-name>servlet</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.madhawa.services</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <listener>
    <listener-class>com.windofkeltia.servlet.ServletInitializer</listener-class>
  </listener>

  <servlet-mapping>
    <servlet-name>servlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

Other, implied sources...

...as blanks to fill in, rename, etc.

Servlet.java:
package com.windofkeltia.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServlet;
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;
import javax.ws.rs.core.Response;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The point of entry into this servlet.
 * @author Russell Bateman
 */
@Path( "" )
public class Servlet extends HttpServlet
{
  private final static Logger log        = LoggerFactory.getLogger( Servlet.class );
  private static final int    STATUS_500 = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();

  @GET
  @Produces( MediaType.TEXT_PLAIN )
  public String getStatusInPlainText( @Context HttpServletRequest request )
  {
    return Status.getStatusInPlainText( Status.readManifest( request.getServletContext() ) );
  }
}
Status.java:
package com.windofkeltia.servlet;

import java.io.IOException;
import java.util.Calendar;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.servlet.ServletContext;

import static java.util.Objects.nonNull;

public class Status
{
  private static final String COPYRIGHT     = "Copyright ";
  private static final String COPY_PLAIN    = "(c) ";
  private static final String COMPANY       = "by Etretat Logiciels, LLC.";
  private static final String RIGHTS        = "Proprietary and confidential. All rights reserved.";
  private static final String PURPOSE       = "Delivers servlet support to HTTP clients.";
  private static final String MANIFEST_PATH = "/META-INF/MANIFEST.MF";

  protected static String getStatusInPlainText( final String MANIFEST )
  {
    StringBuilder sb = new StringBuilder();
    sb.append( "The servlet 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 = "2020";
    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( MANIFEST_PATH ) );
      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 );
  }
}
ServletInitializer.java:
package com.windofkeltia.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Initialize when the application starts (and stops).
 * @author Russell Bateman
 */
public class ServletInitializer implements ServletContextListener
{
  private static final Logger log = LoggerFactory.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();
    Servlet        instance = new Servlet();
    try
    {
      /* do some initialization here */;
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
    final String COPYRIGHT_AND_STATUS = Status.readManifest( context );
    log.info( "###########################################);
    log.info( "Initializing servlet application context..." );
    log.info( Status.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 servlet application context!" );
  }
}