Simple Web ApplicationRussell Bateman |
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.
We're going to use:
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
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.
<!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>
<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>
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(); } }
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.
russ@tirion ~/dev $ tar -zxf apache-tomcat-9.0.45.tar.gz
You should have effectuated the following Run/Debug Configurations. Click to see them full size.
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...
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.
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.
...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>
...that came out of the original discussion in the tutorial. Below you see some annotations, or keywords prepended with an ærobase (@).
This status code is the most annoying and hardest to debug. Things to look for as you try to tie it down:
<init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>path to your service code</param-value> </init-param>
<servlet-mapping> <servlet-name>ccd-validator</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
If you're getting 406, you might examine
@GET @Produces( MediaType.TEXT_PLAIN ) public String getStatusInPlainText( @Context HttpServletRequest request ) { return ... }
$ curl http://localhost:8080/simple-servlet/ -H 'Accept: test/plain'
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):
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!" ); } }
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
com.etretatlogiciels.servlet.ServletInitializer
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.
...
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.
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 ) }
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 ) }
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
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..
<?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>
<!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>
...as blanks to fill in, rename, etc.
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() ) ); } }
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 ); } }
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!" ); } }