View Javadoc
1   /*
2    * @(#)JnlpFileHandler.java	1.12 05/11/17
3    * 
4    * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions are met:
8    *
9    * -Redistribution of source code must retain the above copyright notice, this
10   *  list of conditions and the following disclaimer.
11   *
12   * -Redistribution in binary form must reproduce the above copyright notice,
13   *  this list of conditions and the following disclaimer in the documentation
14   *  and/or other materials provided with the distribution.
15   *
16   * Neither the name of Sun Microsystems, Inc. or the names of contributors may
17   * be used to endorse or promote products derived from this software without
18   * specific prior written permission.
19   *
20   * This software is provided "AS IS," without a warranty of any kind. ALL
21   * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
22   * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
23   * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
24   * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
25   * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
26   * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
27   * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
28   * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
29   * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
30   * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
31   *
32   * You acknowledge that this software is not designed, licensed or intended
33   * for use in the design, construction, operation or maintenance of any
34   * nuclear facility.
35   */
36  
37  package jnlp.sample.servlet;
38  
39  import org.w3c.dom.Document;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  
44  import javax.servlet.ServletContext;
45  import javax.servlet.http.HttpServletRequest;
46  import javax.servlet.http.HttpUtils;
47  import javax.xml.parsers.DocumentBuilder;
48  import javax.xml.parsers.DocumentBuilderFactory;
49  import javax.xml.transform.Transformer;
50  import javax.xml.transform.TransformerFactory;
51  import javax.xml.transform.dom.DOMSource;
52  import javax.xml.transform.stream.StreamResult;
53  import java.io.BufferedReader;
54  import java.io.ByteArrayInputStream;
55  import java.io.IOException;
56  import java.io.InputStreamReader;
57  import java.io.StringWriter;
58  import java.net.URL;
59  import java.net.URLConnection;
60  import java.util.Calendar;
61  import java.util.Date;
62  import java.util.HashMap;
63  import java.util.TimeZone;
64  
65  /* The JNLP file handler implements a class that keeps
66   * track of JNLP files and their specializations
67   */
68  public class JnlpFileHandler
69  {
70      private static final String JNLP_MIME_TYPE = "application/x-java-jnlp-file";
71  
72      private static final String HEADER_LASTMOD = "Last-Modified";
73  
74      private ServletContext _servletContext;
75  
76      private Logger _log = null;
77  
78      private HashMap _jnlpFiles = null;
79  
80      /**
81       * Initialize JnlpFileHandler for the specific ServletContext
82       */
83      public JnlpFileHandler( ServletContext servletContext, Logger log )
84      {
85          _servletContext = servletContext;
86          _log = log;
87          _jnlpFiles = new HashMap();
88      }
89  
90      private static class JnlpFileEntry
91      {
92          // Response
93          DownloadResponse _response;
94  
95          // Keeps track of cache is out of date
96          private long _lastModified;
97  
98          // Constructor
99          JnlpFileEntry( DownloadResponse response, long lastmodfied )
100         {
101             _response = response;
102             _lastModified = lastmodfied;
103         }
104 
105         public DownloadResponse getResponse()
106         {
107             return _response;
108         }
109 
110         long getLastModified()
111         {
112             return _lastModified;
113         }
114     }
115 
116     /* Main method to lookup an entry */
117     public synchronized DownloadResponse getJnlpFile( JnlpResource jnlpres, DownloadRequest dreq )
118         throws IOException
119     {
120         String path = jnlpres.getPath();
121         URL resource = jnlpres.getResource();
122         long lastModified = jnlpres.getLastModified();
123 
124         _log.addDebug( "lastModified: " + lastModified + " " + new Date( lastModified ) );
125         if ( lastModified == 0 )
126         {
127             _log.addWarning( "servlet.log.warning.nolastmodified", path );
128         }
129 
130         // fix for 4474854:  use the request URL as key to look up jnlp file
131         // in hash map
132         String reqUrl = HttpUtils.getRequestURL( dreq.getHttpRequest() ).toString();
133 
134         // Check if entry already exist in HashMap
135         JnlpFileEntry jnlpFile = (JnlpFileEntry) _jnlpFiles.get( reqUrl );
136 
137         if ( jnlpFile != null && jnlpFile.getLastModified() == lastModified )
138         {
139             // Entry found in cache, so return it
140             return jnlpFile.getResponse();
141         }
142 
143         // Read information from WAR file
144         long timeStamp = lastModified;
145         String mimeType = _servletContext.getMimeType( path );
146         if ( mimeType == null )
147         {
148             mimeType = JNLP_MIME_TYPE;
149         }
150 
151         StringBuilder jnlpFileTemplate = new StringBuilder();
152         URLConnection conn = resource.openConnection();
153         BufferedReader br = new BufferedReader( new InputStreamReader( conn.getInputStream(), "UTF-8" ) );
154         String line = br.readLine();
155         if ( line != null && line.startsWith( "TS:" ) )
156         {
157             timeStamp = parseTimeStamp( line.substring( 3 ) );
158             _log.addDebug( "Timestamp: " + timeStamp + " " + new Date( timeStamp ) );
159             if ( timeStamp == 0 )
160             {
161                 _log.addWarning( "servlet.log.warning.notimestamp", path );
162                 timeStamp = lastModified;
163             }
164             line = br.readLine();
165         }
166         while ( line != null )
167         {
168             jnlpFileTemplate.append( line );
169             line = br.readLine();
170         }
171 
172         String jnlpFileContent = specializeJnlpTemplate( dreq.getHttpRequest(), path, jnlpFileTemplate.toString() );
173 
174         // Convert to bytes as a UTF-8 encoding
175         byte[] byteContent = jnlpFileContent.getBytes( "UTF-8" );
176 
177         // Create entry
178         DownloadResponse resp =
179             DownloadResponse.getFileDownloadResponse( byteContent, mimeType, timeStamp, jnlpres.getReturnVersionId() );
180         jnlpFile = new JnlpFileEntry( resp, lastModified );
181         _jnlpFiles.put( reqUrl, jnlpFile );
182 
183         return resp;
184     }
185 
186     /* Main method to lookup an entry (NEW for JavaWebStart 1.5+) */
187     public synchronized DownloadResponse getJnlpFileEx( JnlpResource jnlpres, DownloadRequest dreq )
188         throws IOException
189     {
190         String path = jnlpres.getPath();
191         URL resource = jnlpres.getResource();
192         long lastModified = jnlpres.getLastModified();
193 
194         _log.addDebug( "lastModified: " + lastModified + " " + new Date( lastModified ) );
195         if ( lastModified == 0 )
196         {
197             _log.addWarning( "servlet.log.warning.nolastmodified", path );
198         }
199 
200         // fix for 4474854:  use the request URL as key to look up jnlp file
201         // in hash map
202         String reqUrl = HttpUtils.getRequestURL( dreq.getHttpRequest() ).toString();
203         // SQE: To support query string, we changed the hash key from Request URL to (Request URL + query string)
204         if ( dreq.getQuery() != null )
205         {
206             reqUrl += dreq.getQuery();
207         }
208 
209         // Check if entry already exist in HashMap
210         JnlpFileEntry jnlpFile = (JnlpFileEntry) _jnlpFiles.get( reqUrl );
211 
212         if ( jnlpFile != null && jnlpFile.getLastModified() == lastModified )
213         {
214             // Entry found in cache, so return it
215             return jnlpFile.getResponse();
216         }
217 
218         // Read information from WAR file
219         long timeStamp = lastModified;
220         String mimeType = _servletContext.getMimeType( path );
221         if ( mimeType == null )
222         {
223             mimeType = JNLP_MIME_TYPE;
224         }
225 
226         StringBuilder jnlpFileTemplate = new StringBuilder();
227         URLConnection conn = resource.openConnection();
228         BufferedReader br = new BufferedReader( new InputStreamReader( conn.getInputStream(), "UTF-8" ) );
229         String line = br.readLine();
230         if ( line != null && line.startsWith( "TS:" ) )
231         {
232             timeStamp = parseTimeStamp( line.substring( 3 ) );
233             _log.addDebug( "Timestamp: " + timeStamp + " " + new Date( timeStamp ) );
234             if ( timeStamp == 0 )
235             {
236                 _log.addWarning( "servlet.log.warning.notimestamp", path );
237                 timeStamp = lastModified;
238             }
239             line = br.readLine();
240         }
241         while ( line != null )
242         {
243             jnlpFileTemplate.append( line );
244             line = br.readLine();
245         }
246 
247         String jnlpFileContent = specializeJnlpTemplate( dreq.getHttpRequest(), path, jnlpFileTemplate.toString() );
248 
249         /* SQE: We need to add query string back to href in jnlp file. We also need to handle JRE requirement for
250        * the test. We reconstruct the xml DOM object, modify the value, then regenerate the jnlpFileContent.
251        */
252         String query = dreq.getQuery();
253         String testJRE = dreq.getTestJRE();
254         _log.addDebug( "Double check query string: " + query );
255         // For backward compatibility: Always check if the href value exists.
256         // Bug 4939273: We will retain the jnlp template structure and will NOT add href value. Above old
257         // approach to always check href value caused some test case not run.
258         if ( query != null )
259         {
260             byte[] cb = jnlpFileContent.getBytes( "UTF-8" );
261             ByteArrayInputStream bis = new ByteArrayInputStream( cb );
262             try
263             {
264                 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
265                 DocumentBuilder builder = factory.newDocumentBuilder();
266                 Document document = builder.parse( bis );
267                 if ( document != null && document.getNodeType() == Node.DOCUMENT_NODE )
268                 {
269                     boolean modified = false;
270                     Element root = document.getDocumentElement();
271 
272                     if ( root.hasAttribute( "href" ) )
273                     {
274                         String href = root.getAttribute( "href" );
275                         root.setAttribute( "href", href + "?" + query );
276                         modified = true;
277                     }
278                     // Update version value for j2se tag
279                     if ( testJRE != null )
280                     {
281                         NodeList j2seNL = root.getElementsByTagName( "j2se" );
282                         if ( j2seNL != null )
283                         {
284                             Element j2se = (Element) j2seNL.item( 0 );
285                             String ver = j2se.getAttribute( "version" );
286                             if ( ver.length() > 0 )
287                             {
288                                 j2se.setAttribute( "version", testJRE );
289                                 modified = true;
290                             }
291                         }
292                     }
293                     TransformerFactory tFactory = TransformerFactory.newInstance();
294                     Transformer transformer = tFactory.newTransformer();
295                     DOMSource source = new DOMSource( document );
296                     StringWriter sw = new StringWriter();
297                     StreamResult result = new StreamResult( sw );
298                     transformer.transform( source, result );
299                     jnlpFileContent = sw.toString();
300                     _log.addDebug( "Converted jnlpFileContent: " + jnlpFileContent );
301                     // Since we modified the file on the fly, we always update the timestamp value with current time
302                     if ( modified )
303                     {
304                         timeStamp = new java.util.Date().getTime();
305                         _log.addDebug( "Last modified on the fly:  " + timeStamp );
306                     }
307                 }
308             }
309             catch ( Exception e )
310             {
311                 _log.addDebug( e.toString(), e );
312             }
313         }
314 
315         // Convert to bytes as a UTF-8 encoding
316         byte[] byteContent = jnlpFileContent.getBytes( "UTF-8" );
317 
318         // Create entry
319         DownloadResponse resp =
320             DownloadResponse.getFileDownloadResponse( byteContent, mimeType, timeStamp, jnlpres.getReturnVersionId() );
321         jnlpFile = new JnlpFileEntry( resp, lastModified );
322         _jnlpFiles.put( reqUrl, jnlpFile );
323 
324         return resp;
325     }
326 
327     /* This method performs the following substituations
328     *  $$name
329     *  $$codebase
330     *  $$context
331     */
332     private String specializeJnlpTemplate( HttpServletRequest request, String respath, String jnlpTemplate )
333     {
334         String urlprefix = getUrlPrefix( request );
335         int idx = respath.lastIndexOf( '/' ); //
336         String name = respath.substring( idx + 1 );    // Exclude /
337         String codebase = respath.substring( 0, idx + 1 ); // Include /
338         jnlpTemplate = substitute( jnlpTemplate, "$$name", name );
339         // fix for 5039951: Add $$hostname macro
340         jnlpTemplate = substitute( jnlpTemplate, "$$hostname", request.getServerName() );
341         jnlpTemplate = substitute( jnlpTemplate, "$$codebase", urlprefix + request.getContextPath() + codebase );
342         jnlpTemplate = substitute( jnlpTemplate, "$$context", urlprefix + request.getContextPath() );
343         // fix for 6256326: add $$site macro to sample jnlp servlet
344         jnlpTemplate = substitute( jnlpTemplate, "$$site", urlprefix );
345         return jnlpTemplate;
346     }
347 
348     // This code is heavily inspired by the stuff in HttpUtils.getRequestURL
349     private String getUrlPrefix( HttpServletRequest req )
350     {
351         StringBuilder url = new StringBuilder();
352         String scheme = req.getScheme();
353         int port = req.getServerPort();
354         url.append( scheme );        // http, https
355         url.append( "://" );
356         url.append( req.getServerName() );
357         if ( ( scheme.equals( "http" ) && port != 80 ) || ( scheme.equals( "https" ) && port != 443 ) )
358         {
359             url.append( ':' );
360             url.append( req.getServerPort() );
361         }
362         return url.toString();
363     }
364 
365     private String substitute( String target, String key, String value )
366     {
367         int start = 0;
368         do
369         {
370             int idx = target.indexOf( key, start );
371             if ( idx == -1 )
372             {
373                 return target;
374             }
375             target = target.substring( 0, idx ) + value + target.substring( idx + key.length() );
376             start = idx + value.length();
377         }
378         while ( true );
379     }
380 
381     /**
382      * Parses a ISO 8601 Timestamp. The format of the timestamp is:
383      * <p/>
384      * YYYY-MM-DD hh:mm:ss  or   YYYYMMDDhhmmss
385      * <p/>
386      * Hours (hh) is in 24h format. ss are optional. Time are by default relative
387      * to the current timezone. Timezone information can be specified
388      * by:
389      * <p/>
390      * - Appending a 'Z', e.g., 2001-12-19 12:00Z
391      * - Appending +hh:mm, +hhmm, +hh, -hh:mm -hhmm, -hh to
392      * indicate that the locale timezone used is either the specified
393      * amound before or after GMT. For example,
394      * <p/>
395      * 12:00Z = 13:00+1:00 = 0700-0500
396      * <p/>
397      * The method returns 0 if it cannot pass the string. Otherwise, it is
398      * the number of milliseconds size sometime in 1969.
399      */
400     private long parseTimeStamp( String timestamp )
401     {
402         int YYYY = 0;
403         int MM = 0;
404         int DD = 0;
405         int hh = 0;
406         int mm = 0;
407         int ss = 0;
408 
409         timestamp = timestamp.trim();
410         try
411         {
412             // Check what format is used
413             if ( matchPattern( "####-##-## ##:##", timestamp ) )
414             {
415                 YYYY = getIntValue( timestamp, 0, 4 );
416                 MM = getIntValue( timestamp, 5, 7 );
417                 DD = getIntValue( timestamp, 8, 10 );
418                 hh = getIntValue( timestamp, 11, 13 );
419                 mm = getIntValue( timestamp, 14, 16 );
420                 timestamp = timestamp.substring( 16 );
421                 if ( matchPattern( ":##", timestamp ) )
422                 {
423                     ss = getIntValue( timestamp, 1, 3 );
424                     timestamp = timestamp.substring( 3 );
425                 }
426             }
427             else if ( matchPattern( "############", timestamp ) )
428             {
429                 YYYY = getIntValue( timestamp, 0, 4 );
430                 MM = getIntValue( timestamp, 4, 6 );
431                 DD = getIntValue( timestamp, 6, 8 );
432                 hh = getIntValue( timestamp, 8, 10 );
433                 mm = getIntValue( timestamp, 10, 12 );
434                 timestamp = timestamp.substring( 12 );
435                 if ( matchPattern( "##", timestamp ) )
436                 {
437                     ss = getIntValue( timestamp, 0, 2 );
438                     timestamp = timestamp.substring( 2 );
439                 }
440             }
441             else
442             {
443                 // Unknown format
444                 return 0;
445             }
446         }
447         catch ( NumberFormatException e )
448         {
449             // Bad number
450             return 0;
451         }
452 
453         String timezone = null;
454         // Remove timezone information
455         timestamp = timestamp.trim();
456         if ( timestamp.equalsIgnoreCase( "Z" ) )
457         {
458             timezone = "GMT";
459         }
460         else if ( timestamp.startsWith( "+" ) || timestamp.startsWith( "-" ) )
461         {
462             timezone = "GMT" + timestamp;
463         }
464 
465         if ( timezone == null )
466         {
467             // Date is relative to current locale
468             Calendar cal = Calendar.getInstance();
469             cal.set( YYYY, MM - 1, DD, hh, mm, ss );
470             return cal.getTime().getTime();
471         }
472         else
473         {
474             // Date is relative to a timezone
475             Calendar cal = Calendar.getInstance( TimeZone.getTimeZone( timezone ) );
476             cal.set( YYYY, MM - 1, DD, hh, mm, ss );
477             return cal.getTime().getTime();
478         }
479     }
480 
481     private int getIntValue( String key, int start, int end )
482     {
483         return Integer.parseInt( key.substring( start, end ) );
484     }
485 
486     private boolean matchPattern( String pattern, String key )
487     {
488         // Key must be longer than pattern
489         if ( key.length() < pattern.length() )
490         {
491             return false;
492         }
493         for ( int i = 0; i < pattern.length(); i++ )
494         {
495             char format = pattern.charAt( i );
496             char ch = key.charAt( i );
497             if ( !( ( format == '#' && Character.isDigit( ch ) ) || ( format == ch ) ) )
498             {
499                 return false;
500             }
501         }
502         return true;
503     }
504 }
505 
506