2017-03-28 4 views
3

J'ai un ensemble de programmes Java Applet qui s'exécutent sur Tomcat. Ces programmes gardent la trace des événements «tournois» informels de golf, y compris les compétitions amicales de joueurs. Bien que les détails du programme ne soient pas importants, le jeu de codes comprend plus de 30 000 lignes de source. J'ai choisi Java comme langage d'implémentation pour la portabilité et pour éviter les problèmes de maintenance. J'utilise Tomcat pour déployer l'application et javascript pour invoquer les applets. Toutes mes applets utilisent des paramètres, tels que le nom de l'événement, le nom du cours et la date de lecture.Comment mettre à niveau une applet Java Tomcat pour utiliser JNLP?

Malheureusement, les modifications de navigateur et de navigateur ont maintenant causé des problèmes de maintenance pour mon application. Le premier problème était que java a ajouté une exigence que les fichiers jar soient signés. Le deuxième problème était que d'abord Chrome et maintenant Firefox ont supprimé la prise en charge des plugins NPAPI, qui supprimaient essentiellement le support des applets Java de html.

JNLP (Java Web Start) est le nouveau remplacement. Les deux problèmes étaient quelque peu difficiles à résoudre, car il n'existait pas de documentation claire, étape par étape, détaillant exactement ce qui devait réellement être fait.

Il peut y avoir des façons différentes, voire meilleures, de migrer des applets vers JNLP, mais les procédures décrites ici fonctionnent et sont terminées. Cependant, en les décrivant je dois supposer que vous savez déjà comment créer une application web Java car il n'y a pas besoin de mettre à jour quelque chose que vous n'avez pas déjà.

Je travaille avec Tomcat dans l'environnement Windows Cygwin. Mon exemple de script mkJavaKey utilise explicitement cet environnement mais tous les codes Java et javascript sont portables. Tomcat utilise web.xml pour définir la manière dont les servlets sont invoqués. Si vous utilisez une méthode de déploiement différente, mon fichier web.xml devrait au moins fonctionner comme point de départ.

Répondre

3

Pourquoi avez-vous besoin de signer un fichier JAR?

Je ne peux pas répondre à cette partie de la question. Mais, pour toute application non triviale, vous devrez aller au moins à travers la procédure pour auto-signer vos fichiers jar même si cette auto-signature ne fournit aucune réelle sécurité supplémentaire. Tout le monde peut auto-signer une application en utilisant les outils fournis dans le Java Development Kit. Les certificats auto-signés conviennent au travail de développement, mais vous devrez cliquer sur une case à cocher d'acceptation des risques chaque fois que vous exécutez votre application. OK, mon application est non-triviale et j'ai besoin de mes fichiers jar signés. Quelle est la procédure?

Voici la réponse rapide: C'est un processus en deux étapes. Vous devez d'abord utiliser le programme keytool pour créer les informations d'identification nécessaires, puis utiliser l'outil jarsigner pour signer vos fichiers jar. Vous avez seulement besoin de créer des informations d'identification de temps en temps, mais vous devez signer chaque fichier jar déployé.

Pour créer ces informations d'identification (un certificat auto-signé) utiliser:

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

Cela crée un certificat nommé .keystore dans votre répertoire personnel qui est bon pour cinq ans. Vous devez répondre à ses invites et j'ai utilisé "mot de passe" comme mot de passe. Comme je n'utilise ce certificat que pour les fichiers jar autosignés, la sécurité n'est pas un gros problème. Le paramètre de validité indique combien de temps (en jours) le certificat est valide.

Chaque fois que vous mettez à jour un fichier jar, vous devez le signer.En supposant que vous êtes dans votre répertoire de distribution et que vous devez signer applet.jar, utilisez:

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

Le « mot de passe » après -storepass correspond le mot de passe que vous avez utilisé avec keytool, et le « mydomain » correspond au paramètre keytool -alias. Vous devrez spécifier le paramètre -tsa (Time Stamp Authority) et http://timestamp.digicert.com est (ou était au moins) un disponible publiquement. Je ne sais pas exactement ce que fait un TSA ou pourquoi vous en avez besoin, mais jarsigner n'est pas content sans cela, ne le fera pas par défaut, et ne documentera pas directement comment en trouver un.

Vous pouvez maintenant utiliser ou ignorer le fichier de traitement par lots suivant. Je l'ai créé parce que quand j'avais besoin de créer un nouveau certificat (mon certificat original a expiré) j'avais oublié comment le créer. J'espère que nous pourrons trouver ce fichier batch la prochaine fois que nous en aurons besoin, peut-être dans cinq ans.

#!/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 $? 

Remarques: Mon script setupJAVA définit la variable d'environnement JAVA_HOME. Pour Linux, utilisez $HOME au lieu de $HOMEPATH et ignorez les sections cygpath. Ils convertissent les formats de nom de fichier Linux et Windows dans l'environnement Cygwin.

Vous devrez signer vos fichiers jar chaque fois que vous les installerez. Pour automatiser cela, j'ai modifié mon Makefile pour le faire. Voici l'extrait de code make je:

.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) 

Les $(OBDIR) et $(DEPLOYDIR) variables ne sont pas pertinentes à cette discussion. Ce sont des chemins de répertoire définis dans mon environnement de construction basé sur Makefile.

Comment migrez-vous les Applets vers le nouvel environnement JNLP? Maintenant que nous avons des jarfiles auto-signés, nous pouvons commencer à comprendre comment les exécuter. De nombreux navigateurs ne prennent plus en charge NPAPI, donc la balise <applet> ne fonctionnera pas. DeployavaJava.runApplet() ne sera pas déployé. Je ne vais pas comprendre pourquoi le support NPAPI a été abandonné, juste ce qui doit être fait pour faire fonctionner vos applications existantes.

Le plus gros problème que j'ai trouvé en migrant mon code était que, finalement, j'ai dû créer des fichiers .jnlp plutôt que des fichiers .html. Je vais vous montrer comment faire, en décrivant le code que j'ai modifié et ajouté.

C'est le (désormais obsolète) code javascript je pour générer 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(); 
} 

Nous ne avons pas besoin de voir html généré par mon servlet qui comprend le bouton du formulaire utilisé pour appeler la fonction cardEvents. Il est similaire à la génération du bouton "DONE" et n'a pas besoin d'être changé.

Il aurait dû être assez simple de simplement convertir ce javascript pour générer un fichier jnlp. Ce n'était pas possible, ou du moins je ne pouvais pas trouver d'exemples pratiques de la façon de le faire et je ne pouvais pas trouver un moyen de le faire en modifiant l'un des exemples cassés. L'instruction window.open() ajouterait toujours les sections <html> et <body> même si je ne voulais générer que xml jnlp. J'ai également essayé document.open("application/x-java-jnlp-file"). Même si le type mime était spécifié, les sections html et corps indésirables étaient toujours présentes.

Aucune de la documentation trouvée n'a montré comment générer dynamiquement le fichier .jnlp dont j'avais besoin, qui comprenait des paramètres d'applet sélectionnés par l'utilisateur. Voici le travail que j'ai utilisé à la place.

j'ai remplacé la génération html applet.js avec ceci:

//------------------------------------------------------------------------ 
// 
// 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!) 
} 

Cela génère une URL sous la forme de Applet.jnlp,className,description,parm=value,parm=value,.... J'ai ensuite créé une nouvelle servlet appelée AppletServlet.java. L'URL qui lui est transmise fournit toutes les informations nécessaires pour générer le fichier .jnlp. Ce code suit l'exemple de structure de servlet standard où doGet est appelé pour gérer la requête. Voici le 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 est mon drapeau "debug activé", et log() écrit un message de débogage à stdout. Dans cette nouvelle version de code, la hauteur et la largeur ne sont pas transmises en tant que paramètres, mais sont codées en dur à la place. Il s'est avéré que dans la version HTML "100%" a toujours été utilisé comme la hauteur et la largeur et a bien fonctionné. Pour certains (inconnus de moi), mes fenêtres d'applet sont tronquées en bas et éventuellement sur la droite lorsqu'elles sont appelées en utilisant le code .jnlp avec 100% de hauteur et de largeur. J'utilise ces nouveaux paramètres de hauteur et de largeur pour contourner ce problème de formatage.

Afin d'invoquer mon nouveau AppletServlet, je modifié mon fichier web.xml:

<?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> 

Cela provoque AppletServlet à invoquer pour toute URL Applet.jnlp. Les navigateurs ignorent la chaîne de requête et traitent le résultat comme si le nom du fichier était Applet.jnlp.

Pour un fonctionnement plus fluide, vous devez définir vos associations de fichiers Windows afin que les fichiers .jnlp invoquent Java (TM) Web Start Launcher. Dans Windows, votre JWS Launcher est C:\Program Files\java\jre*\bin\javaws.exe (utilisez votre dernier dossier jre.) De plus, si vous utilisez Chrome, votre répertoire de téléchargement contiendra les fichiers Applet.jnlp générés. Vous devrez les nettoyer de temps en temps.

Ceci termine le processus de migration. Aucune applet n'a été endommagée (ou modifiée) dans cette migration, de sorte que la plus grande partie des 30 000 lignes source est restée inchangée.

Bien que j'aie utilisé le copier-coller du code opérationnel pour créer les exemples, il est possible que des fautes de frappe se soient glissées. Veuillez commenter si vous trouvez quelque chose d'incorrect, de manquant ou de flou.