How to update Tomcat Java applet to use JNLP?

I have a set of Apple Applet programs that run on Tomcat. These programs track informal "tournament" golf tournaments, including friendly player competitions.

Although the program data is not important, the code set includes over 30,000 source lines. I chose Java as my implementation language for portability and to avoid maintenance issues. I am using Tomcat to deploy my application and javascript to call applets. All my applets use parameters like event name, course name, and play date.

Unfortunately, changes in java and browsers have now caused service issues for my application. The first problem was that java added a requirement to sign jar files. The second issue was that the first Chrome and Firefox have now removed support for NPAPI plugins, which substantially removed Java Applet support from html.

JNLP ( Java Web Start ) is a new replacement. Both problems were quite difficult to fix because there was no clear step-by-step documentation detailing what actually needs to be done.

There may be different, even better ways of porting applets to JNLP, but the procedures described here work and are complete. However, in describing them, I must assume that you already know how to create a Java application, since there is no need to update what you do not already have.

I am working with Tomcat on Windows Cygwin environment. My mkJavaKey script example uses this framework explicitly, but all Java and javascript code is portable. Tomcat uses web.xml to determine how Servlets is called. If you are using a different deployment method, my web.xml file should at least work as a starting point.

+3


source to share


1 answer


Why do you need to sign a jar file?

I cannot answer this part of the question. But for any non-trivial application, you need to go through at least a procedure to self-sign the jar files , although this self- signing does not provide any real additional security. Any user can self-sign an application using the tools provided in the Java Development Kit. Self-signed certificates are fine for development work, but each time you launch the application, you will need to click the Risk Accept checkbox.

OK, my application is non-trivial and I need my jar files. What is the procedure?

Here's a quick answer: This is a two-step process. First, you use the keytool program to create the required credentials and then use the jarsigner tool to sign your jar files. You only need to create credentials from time to time, but you need to sign each deployed jar file.

To create these credentials (self-signed certificate) use:

$JAVA_HOME/bin/keytool -genkeypair -keyalg RSA -keysize 2048 -alias mydomain -validity 1825

      

Creates a certificate with a name .keystore

in your home directory that fits five years. You have to respond to his prompts, and I used "password" as my password. Since I only use this certificate for self-configuring jar files, security isn't a big issue. The validity parameter specifies how long (in days) the certificate is valid.

Every time you update the jar file you need to sign it. Assuming you are in your distribution directory and need to sign applet.jar

, use:

$JAVA_HOME/bin/jarsigner -tsa http://timestamp.digicert.com -storepass password applet.jar mydomain

      

"password" after -storepass

corresponds to the password you used with keytool, and "mydomain" corresponds to the keytool parameter -alias

. You will need to specify the -tsa (Time Stamp Authority) parameter and http://timestamp.digicert.com (or at least was) one publicly available. I don't know exactly what TSA does or why you need it, but jarsigner wasn't happy without it, it won't and won't directly document how to find it.

You can now use or ignore the next batch file. I created it because when I needed to create a new certificate (my original certificate expired) I forgot how to create it. Hopefully we can find this batch file next time we need it, perhaps five years from now.

#!/bin/bash
#
# Title-
#        mkJavaKey
#
# Function-
#        Create a new key using $JAVA_HOME/bin/keytool
#
# Usage-
#        mkJavaKey ## CYGWIN ONLY ##
#        (This is required when jarsigner complains about an expired key.)
#        NOTE: This *REMOVES* and *REPLACES* your existing .keystore file!
#
#######

##########################################################################
# Environment check
if [ -z "$JAVA_HOME" ] ; then
  . setupJAVA ## (This personal script sets JAVA_HOME)
  if [ -z "$JAVA_HOME" ] ; then
    echo "JAVA_HOME environment variable missing"
    exit 1
  fi
fi

if [ -z "$HOMEPATH" ] ; then
  echo "HOMEPATH environment variable missing"
  echo "Try export HOMEPATH=\Users\myname"
  exit 1
fi

home_path=`cygpath --path --unix C:$HOMEPATH`
PGM=$JAVA_HOME/bin/keytool
if [ ! -x "$PGM" ] ; then
  echo "$PGM not executable"
  exit 1
fi

##########################################################################
# Create a new .keystore
set -x
rm -Rf $home_path/.keystore
$PGM -genkeypair -keyalg RSA -keysize 2048 -alias mydomain -validity 1825
exit $?

      

Notes. My setup JAVA script is setting an environment variable JAVA_HOME

. For Linux, use $HOME

instead $HOMEPATH

and skip the sections cygpath

. They convert between Linux and Windows file formats in Cygwin environment.

You will need to sign your jar files every time you install them. To automate this, I modified my Makefile to do this. Here is the make code snippet I used:

.PHONY: golfer.install
golfer.install: test golfer
: (Not relevant to discussion)
cp -p $(OBJDIR)/usr/fne/golfer/Applet/applet.jar $(DEPLOYDIR)/webapps/golfer/.
jarsigner -tsa http://timestamp.digicert.com -storepass password "$(shell cygpath --path --windows "$(DEPLOYDIR)/webapps/golfer/applet.jar")" mydomain
: (Not relevant to discussion)

      

Variables $(OBDIR)

and are $(DEPLOYDIR)

not relevant to this discussion. These are the directory paths set in my build environment based on the Makefile.

How do I port applets to the new JNLP environment?

Now that we have our self-signed jarfiles, we can start figuring out how to run them. Many browsers no longer support NPAPI, so the tag <applet>

doesn't work. Also, Java.runApplet () will not be deployed. I will not explain why NPAPI support was removed, what needs to be done to get your existing applications working.



The biggest problem I found while porting my code was that I ended up having to create .jnlp files, not .html files. I will show you how to do this by describing the code that I have modified and added.

This is the (now deprecated) javascript code I used to generate the html:

//------------------------------------------------------------------------
//
// Title-
//       applet.js
//
// Purpose-
//       Common applet javascript.
//
// Last change date-
//       2010/10/19
//
//------------------------------------------------------------------------
var out;     // Output document

//------------------------------------------------------------------------
// appHead
//
// Generate html header for application.
//------------------------------------------------------------------------
function appHead(title,cname,height,width)
{
   var todoWindow= window.open('','','');
   out= todoWindow.document;
   out.write('<html>');
   out.write('<head><title>' + title + '</title></head>');
   out.write('<body>\n');
   out.write('<applet code="' + cname + '.class"');
   out.write('    codebase="./"')
   out.write('    archive="applet.jar,jars/common.jar"');
   out.write('    width="' + width + '" height="' + height + '">\n');
}

//------------------------------------------------------------------------
// appParm
//
// Add parameter information
//------------------------------------------------------------------------
function appParm(name, value)
{
     out.write('        <param-name="' + name + '" value="' + value + '"/>\n');
}

//------------------------------------------------------------------------
// appTail
//
// Generate html trailer information.
//------------------------------------------------------------------------
function appTail()
{
   out.write('Your browser is completely ignoring the &lt;APPLET&gt; tag!\n');
   out.write('</applet>');
   out.write('<form>');
   out.write('<input type="button" value="Done" onclick="window.close()">');
   out.write('</form>');
   out.write('</body>');
   out.write('</html>');
   out.close();
   out= null;
}

//------------------------------------------------------------------------
// cardEvents
//
// Display scorecard for selected date.
//------------------------------------------------------------------------
function cardEvents(eventsID, obj) 
{
   if( obj.selectedIndex == 0 )
   {
     alert("No date selected");
     return;
   }
   appHead('Score card', 'EventsCard', '100%', '100%');
   appParm('events-nick', eventsID);
   appParm('events-date', obj[obj.selectedIndex].value);
   appTail();
   reset();
}

      

We don't need to see the html generated by my servlet that includes the form button used to call the cardEvents function. It is similar to creating the "DONE" button and does not need to be changed.

It should be enough to just translate this javascript to generate a jnlp file. It wasn't possible, or at least I couldn't find any working examples of how to do this, and couldn't find a way to do it by modifying any of the broken examples. The operator window.open()

always added sections <html>

and <body>

even though I only wanted to create jnlp xml. I have also tried document.open("application/x-java-jnlp-file")

. Even though the mime type was specified, the unwanted html and body sections were still present.

None of the documents I found showed how to dynamically generate the .jnlp file I needed, which included user-selected applet options. Here I used work instead.

I replaced the html generation in applet.js with this:

//------------------------------------------------------------------------
//
// Title-
//       applet.js
//
// Purpose-
//       Common applet javascript.
//
// Last change date-
//       2017/03/15
//
//------------------------------------------------------------------------
var out;     // Output URL

//------------------------------------------------------------------------
// appHead
//
// Generate application URL header.
//------------------------------------------------------------------------
function appHead(title,cname,height,width)
{
   out= cname + ',' + title;
}

//------------------------------------------------------------------------
// appParm
//
// Generate html parameter information.
//------------------------------------------------------------------------
function appParm(name, value)
{
   out= out + ',' + name + '=' + value;
}

//------------------------------------------------------------------------
// appTail
//
// Generate html trailer information.
//------------------------------------------------------------------------
function appTail()
{
   var specs= 'menubar=yes,toolbar=yes';
   window.open('Applet.jnlp?' + out, '_self', specs);
}

//------------------------------------------------------------------------
// cardEvents
//
// Display scorecard for selected date.
//------------------------------------------------------------------------
function cardEvents(eventsID, obj)
{
    // (UNCHANGED!)
}

      

This creates a URL in the form Applet.jnlp,className,description,parm=value,parm=value,...

.

Then I created a new servlet named AppletServlet.java. The URL passed to it provides all the information needed to create the .jnlp file. This code follows the standard servlet structure, where doGet is called to process the request. Here's the code:

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.doGet
//
// Purpose-
//       Called for each HTTP GET request.
//
//------------------------------------------------------------------------
public void
   doGet(                           // Handle HTTP "GET" request
     HttpServletRequest  req,       // Request information
     HttpServletResponse res)       // Response information
   throws ServletException, IOException
{
   String q= req.getQueryString();
   if( debug ) log("doGet("+q+")");

   res.setContentType("text/html");

   query(req, res);
}

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.putError
//
// Purpose-
//       Generate error response.
//
//------------------------------------------------------------------------
public void
   putError(                        // Generate error response
     PrintWriter       out,         // The response writer
     String            msg)         // The error message
{       out.println("<HTML>");
   out.println("<HEAD><TITLE>" + msg + "</TITLE></HEAD>");
   out.println("<BODY>");
   out.println("<H1 align=\"center\">" + msg + "</H1>");
   out.println("</BODY>");
   out.println("</HTML>");
}

//------------------------------------------------------------------------
//
// Method-
//       AppletServlet.query
//
// Purpose-
//       Handle a query.
//
//------------------------------------------------------------------------
protected void
   query(                           // Handle a query
     HttpServletRequest  req,       // Request information
     HttpServletResponse res)       // Response information
   throws ServletException, IOException
{
   String q= req.getQueryString();
   if( debug ) log("query("+q+")");

   PrintWriter out = res.getWriter();
   String BOGUS= "<br> Malformed request: query: '" + q + "'";

   //=====================================================================
   // Applet.jnlp?classname,title,parm=value,parm=value,...
   int index= q.indexOf(',');
   if( index < 0 || index == (q.length() - 1) )
   {
     putError(out, BOGUS);
     return;
   }
   String invoke= q.substring(0, index);

   q= q.substring(index+1);
   index= q.indexOf(',');
   if( index < 0 )
     index= q.length();
   String title= q.substring(0, index);
   title= java.net.URLDecoder.decode(title, "UTF-8");

   // Parameter extraction
   Vector<String> param= new Vector<String>();
   if( index < q.length() )
   {
     q= q.substring(index+1);
     for(;;)
     {
       index= q.indexOf(',');
       if( index < 0 )
         index= q.length();

       String s= q.substring(0, index);
       int x= s.indexOf('=');
       if( x < 0 )
       {
         putError(out, BOGUS);
         return;
       }

       param.add(s);
       if( index >= q.length() )
         break;

       q= q.substring(index+1);
     }
   }

   //---------------------------------------------------------------------
   // We now have enough information to generate the response
   //---------------------------------------------------------------------
   res.setContentType("application/x-java-jnlp-file");
   out.println("<?xml version='1.0' encoding='utf-8'?>");
   out.println("<jnlp spec='1.0+' codebase='http://localhost:8080/golfer'>");
   out.println(" <information>");
   out.println("  <title>" + title + "</title>");
   out.println("  <vendor>My Name</vendor>");
   out.println("  <description>" + title + "</description>");
   out.println(" </information>");
   out.println(" <security><all-permissions/></security>");
   out.println(" <resources>");
   out.println("  <j2se version='1.7+'/>");
   out.println("  <jar href='applet.jar'/>");
   out.println("  <jar href='jars/common.jar'/>");
   out.println(" </resources>");
   out.println(" <applet-desc main-class='" + invoke + "' name='" + title + "'" +
                " height='90%' width='98%'>");

   // Insert applet parameters
   for(int i= 0; i<param.size(); i++)
   {
     String s= param.elementAt(i);
     int    x= s.indexOf('=');
     String n= s.substring(0,x);
     String v= s.substring(x+1);
     out.println("  <param name='" + n+ "' value='" + v + "'/>");
   }
   out.println(" </applet-desc>");
   out.println("</jnlp>");
}

      

Notes: debug

- my flag is "debug enabled" and log()

writes a debug message to stdout. In this new version of the code, the height and width are not passed as parameters, but are hardcoded. It turned out that the HTML version of "100%" always used both height and width and worked well. For some (unknown to me) reasons my applet windows are truncated at the bottom and possibly on the right when called using .jnlp code with 100% height and width. I am using these new height and width options to work around this formatting issue.

To call my new AppletServlet, I changed the web.xml file:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!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>
   <servlet>
     <servlet-name>Applet</servlet-name>
     <servlet-class>usr.fne.golfer.AppletServlet</servlet-class>
     <init-param>
       <param-name>property-path</param-name>
       <param-value>profile</param-value>
     </init-param>
     <init-param>
       <param-name>property-file</param-name>
       <param-value>golfer.pro</param-value>
     </init-param>
     <load-on-startup>30</load-on-startup>
   </servlet>

   <servlet-mapping>
     <servlet-name>Applet</servlet-name>
     <url-pattern>/Applet.jnlp</url-pattern>
   </servlet-mapping>

   : (Other Servlets unchanged) 
</web-app>

      

This results in a call to AppletServlet for any Applet.jnlp url. Browsers ignore the query string and treat the result as if the filename were Applet.jnlp.

For a smoother experience, you need to set your Windows file associations so that the .jnlp

files invoke the Java (TM) Web Launchher. On Windows, your JWS Launcher is C:\Program Files\java\jre*\bin\javaws.exe

(use your latest jre folder.) Also, if you are using Chrome, your download directory will contain the generated Applet.jnlp files. You will need to clean them from time to time.

This completes the migration process. No applets were affected (or changed) in this migration, so the bulk of the 30,000 original lines remained unchanged.

While I have used cut and paste from the opcode to create examples, it is possible that typos could have creeped in. Please comment if you find anything wrong, missing or unclear.

+3


source







All Articles