View Javadoc
1   /*
2    * @(#)ResourceCatalog.java	1.6 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 jnlp.sample.util.VersionID;
40  import jnlp.sample.util.VersionString;
41  import org.w3c.dom.Document;
42  import org.xml.sax.SAXParseException;
43  
44  import javax.servlet.ServletContext;
45  import javax.xml.parsers.DocumentBuilder;
46  import javax.xml.parsers.DocumentBuilderFactory;
47  import java.io.BufferedInputStream;
48  import java.io.File;
49  import java.util.ArrayList;
50  import java.util.HashMap;
51  import java.util.List;
52  
53  public class ResourceCatalog
54  {
55      public static final String VERSION_XML_FILENAME = "version.xml";
56  
57      private Logger _log = null;
58  
59      private ServletContext _servletContext = null;
60  
61      private HashMap _entries;
62  
63      /**
64       * Class to contain the information we know
65       * about a specific directory
66       */
67      static private class PathEntries
68      {
69          /* Version-based entries at this particular path */
70          private List _versionXmlList;
71  
72          private List _directoryList;
73  
74          private List _platformList;
75  
76          /* Last time this entry was updated */
77          private long _lastModified; // Last modified time of entry;
78  
79          public PathEntries( List versionXmlList, List directoryList, List platformList, long lastModified )
80          {
81              _versionXmlList = versionXmlList;
82              _directoryList = directoryList;
83              _platformList = platformList;
84              _lastModified = lastModified;
85          }
86  
87  
88          public void setDirectoryList( List dirList )
89          {
90              _directoryList = dirList;
91          }
92  
93          public List getVersionXmlList()
94          {
95              return _versionXmlList;
96          }
97  
98          public List getDirectoryList()
99          {
100             return _directoryList;
101         }
102 
103         public List getPlatformList()
104         {
105             return _platformList;
106         }
107 
108         public long getLastModified()
109         {
110             return _lastModified;
111         }
112     }
113 
114     public ResourceCatalog( ServletContext servletContext, Logger log )
115     {
116         _entries = new HashMap();
117         _servletContext = servletContext;
118         _log = log;
119     }
120 
121 
122     public JnlpResource lookupResource( DownloadRequest dreq )
123         throws ErrorResponseException
124     {
125         // Split request up into path and name
126         String path = dreq.getPath();
127         String name = null;
128         String dir = null;
129         int idx = path.lastIndexOf( '/' );
130         if ( idx == -1 )
131         {
132             name = path;
133         }
134         else
135         {
136             name = path.substring( idx + 1 ); // Exclude '/'
137             dir = path.substring( 0, idx + 1 ); // Include '/'
138         }
139 
140         // Lookup up already parsed entries, and san directory for entries if neccesary
141         PathEntries pentries = (PathEntries) _entries.get( dir );
142         JnlpResource xmlVersionResPath = new JnlpResource( _servletContext, dir + VERSION_XML_FILENAME );
143         if ( pentries == null ||
144             ( xmlVersionResPath.exists() && xmlVersionResPath.getLastModified() > pentries.getLastModified() ) )
145         {
146             _log.addInformational( "servlet.log.scandir", dir );
147             List dirList = scanDirectory( dir, dreq );
148             // Scan XML file
149             List versionList = new ArrayList();
150             List platformList = new ArrayList();
151             parseVersionXML( versionList, platformList, dir, xmlVersionResPath );
152             pentries = new PathEntries( versionList, dirList, platformList, xmlVersionResPath.getLastModified() );
153             _entries.put( dir, pentries );
154         }
155 
156         // Search for a match
157         JnlpResource[] result = new JnlpResource[1];
158 
159         if ( dreq.isPlatformRequest() )
160         {
161             int sts = findMatch( pentries.getPlatformList(), name, dreq, result );
162             if ( sts != DownloadResponse.STS_00_OK )
163             {
164                 throw new ErrorResponseException( DownloadResponse.getJnlpErrorResponse( sts ) );
165             }
166         }
167         else
168         {
169             // First lookup in versions.xml file
170             int sts1 = findMatch( pentries.getVersionXmlList(), name, dreq, result );
171             if ( sts1 != DownloadResponse.STS_00_OK )
172             {
173                 // Then lookup in directory
174                 int sts2 = findMatch( pentries.getDirectoryList(), name, dreq, result );
175                 if ( sts2 != DownloadResponse.STS_00_OK )
176                 {
177 
178                     // fix for 4450104
179                     // try rescan and see if it helps
180                     pentries.setDirectoryList( scanDirectory( dir, dreq ) );
181                     sts2 = findMatch( pentries.getDirectoryList(), name, dreq, result );
182                     // try again after rescanning directory
183                     if ( sts2 != DownloadResponse.STS_00_OK )
184                     {
185                         // Throw the most specific error code
186                         throw new ErrorResponseException(
187                             DownloadResponse.getJnlpErrorResponse( Math.max( sts1, sts2 ) ) );
188                     }
189                 }
190             }
191         }
192         return result[0];
193     }
194 
195     /**
196      * This method finds the best match, or return the best error code. The
197      * result parameter must be an array with room for one element.
198      * <p/>
199      * If a match is found, the method returns DownloadResponse.STS_00_OK
200      * If one or more entries matches on: name, version-id, os, arch, and locale,
201      * then the one with the highest version-id is set in the result[0] field.
202      * <p/>
203      * If a match is not found, it returns an error code, either: ERR_10_NO_RESOURCE,
204      * ERR_11_NO_VERSION, ERR_20_UNSUP_OS, ERR_21_UNSUP_ARCH, ERR_22_UNSUP_LOCALE,
205      * ERR_23_UNSUP_JRE.
206      */
207     public int findMatch( List list, String name, DownloadRequest dreq, JnlpResource[] result )
208     {
209         if ( list == null )
210         {
211             return DownloadResponse.ERR_10_NO_RESOURCE;
212         }
213         // Setup return values
214         VersionID bestVersionId = null;
215         int error = DownloadResponse.ERR_10_NO_RESOURCE;
216         VersionString vs = new VersionString( dreq.getVersion() );
217         // Iterate through entries
218         for ( Object aList : list )
219         {
220             JnlpResource respath = (JnlpResource) aList;
221             VersionID vid = new VersionID( respath.getVersionId() );
222             int sts = matchEntry( name, vs, dreq, respath, vid );
223             if ( sts == DownloadResponse.STS_00_OK )
224             {
225                 if ( result[0] == null || vid.isGreaterThan( bestVersionId ) )
226                 {
227                     result[0] = respath;
228                     bestVersionId = vid;
229                 }
230             }
231             else
232             {
233                 error = Math.max( error, sts );
234             }
235         }
236         return ( result[0] != null ) ? DownloadResponse.STS_00_OK : error;
237     }
238 
239     public int matchEntry( String name, VersionString vs, DownloadRequest dreq, JnlpResource jnlpres, VersionID vid )
240     {
241         if ( !name.equals( jnlpres.getName() ) )
242         {
243             return DownloadResponse.ERR_10_NO_RESOURCE;
244         }
245         if ( !vs.contains( vid ) )
246         {
247             return DownloadResponse.ERR_11_NO_VERSION;
248         }
249         if ( !prefixMatchLists( jnlpres.getOSList(), dreq.getOS() ) )
250         {
251             return DownloadResponse.ERR_20_UNSUP_OS;
252         }
253         if ( !prefixMatchLists( jnlpres.getArchList(), dreq.getArch() ) )
254         {
255             return DownloadResponse.ERR_21_UNSUP_ARCH;
256         }
257         if ( !prefixMatchLists( jnlpres.getLocaleList(), dreq.getLocale() ) )
258         {
259             return DownloadResponse.ERR_22_UNSUP_LOCALE;
260         }
261         return DownloadResponse.STS_00_OK;
262     }
263 
264 
265     private static boolean prefixMatchStringList( String[] prefixList, String target )
266     {
267         // No prefixes matches everything
268         if ( prefixList == null )
269         {
270             return true;
271         }
272         // No target, but a prefix list does not match anything
273         if ( target == null )
274         {
275             return false;
276         }
277         for ( String aPrefixList : prefixList )
278         {
279             if ( target.startsWith( aPrefixList ) )
280             {
281                 return true;
282             }
283         }
284         return false;
285     }
286 
287     /* Return true if at least one of the strings in 'prefixes' are a prefix
288     * to at least one of the 'keys'.
289     */
290     public boolean prefixMatchLists( String[] prefixes, String[] keys )
291     {
292         // The prefixes are part of the server resources. If none is given,
293         // everything matches
294         if ( prefixes == null )
295         {
296             return true;
297         }
298         // If no os keyes was given, and the server resource is keyed of this,
299         // then return false.
300         if ( keys == null )
301         {
302             return false;
303         }
304         // Check for a match on a key
305         for ( String key : keys )
306         {
307             if ( prefixMatchStringList( prefixes, key ) )
308             {
309                 return true;
310             }
311         }
312         return false;
313     }
314 
315     /**
316      * This method scans the directory pointed to by the
317      * given path and creates a list of ResourcePath elements
318      * that contains information about all the entries
319      * <p/>
320      * The version-based information is encoded in the file name
321      * given the following format:
322      * <p/>
323      * entry ::= <name> __ ( <options> ). <ext>
324      * options ::= <option> ( __ <options>  )?
325      * option  ::= V<version-id>
326      * | O<os>
327      * | A<arch>
328      * | L<locale>
329      */
330 
331 
332     private String jnlpGetPath( DownloadRequest dreq )
333     {
334         // fix for 4474021
335         // try to manuually generate the filename
336         // extract file name
337         String path = dreq.getPath();
338         String filename = path.substring( path.lastIndexOf( "/" ) + 1 );
339         path = path.substring( 0, path.lastIndexOf( "/" ) + 1 );
340         String name = filename;
341         String ext = null;
342 
343         if ( filename.lastIndexOf( "." ) != -1 )
344         {
345             ext = filename.substring( filename.lastIndexOf( "." ) + 1 );
346 
347             filename = filename.substring( 0, filename.lastIndexOf( "." ) );
348 
349         }
350         if ( dreq.getVersion() != null )
351         {
352             filename += "__V" + dreq.getVersion();
353         }
354 
355         String[] temp = dreq.getOS();
356 
357         if ( temp != null )
358         {
359             for ( String aTemp : temp )
360             {
361                 filename += "__O" + aTemp;
362             }
363         }
364 
365         temp = dreq.getArch();
366 
367         if ( temp != null )
368         {
369             for ( String aTemp : temp )
370             {
371                 filename += "__A" + aTemp;
372             }
373         }
374         temp = dreq.getLocale();
375 
376         if ( temp != null )
377         {
378             for ( String aTemp : temp )
379             {
380                 filename += "__L" + aTemp;
381             }
382         }
383 
384         if ( ext != null )
385         {
386             filename += "." + ext;
387         }
388 
389         path += filename;
390 
391         return path;
392     }
393 
394     public List scanDirectory( String dirPath, DownloadRequest dreq )
395     {
396         ArrayList list = new ArrayList();
397 
398         // fix for 4474021
399         if ( _servletContext.getRealPath( dirPath ) == null )
400         {
401             String path = jnlpGetPath( dreq );
402 
403             String name = dreq.getPath().substring( path.lastIndexOf( "/" ) + 1 );
404 
405             JnlpResource jnlpres =
406                 new JnlpResource( _servletContext, name, dreq.getVersion(), dreq.getOS(), dreq.getArch(),
407                                   dreq.getLocale(), path, dreq.getVersion() );
408 
409             // the file does not exist
410             if ( jnlpres.getResource() == null )
411             {
412                 return null;
413             }
414 
415             list.add( jnlpres );
416             return list;
417         }
418         File dir = new File( _servletContext.getRealPath( dirPath ) );
419         _log.addDebug( "File directory: " + dir );
420         if ( dir.exists() && dir.isDirectory() )
421         {
422             File[] entries = dir.listFiles();
423             for ( File entry : entries )
424             {
425                 JnlpResource jnlpres = parseFileEntry( dirPath, entry.getName() );
426                 if ( jnlpres != null )
427                 {
428                     if ( _log.isDebugLevel() )
429                     {
430                         _log.addDebug( "Read file resource: " + jnlpres );
431                     }
432                     list.add( jnlpres );
433                 }
434             }
435         }
436         return list;
437     }
438 
439     private JnlpResource parseFileEntry( String dir, String filename )
440     {
441         int idx = filename.indexOf( "__" );
442         if ( idx == -1 )
443         {
444             return null;
445         }
446 
447         // Cut out name
448         String name = filename.substring( 0, idx );
449         String rest = filename.substring( idx );
450 
451         // Cut out extension
452         idx = rest.lastIndexOf( '.' );
453         String extension = "";
454         if ( idx != -1 )
455         {
456             extension = rest.substring( idx );
457             rest = rest.substring( 0, idx );
458         }
459 
460         // Parse options
461         String versionId = null;
462         ArrayList osList = new ArrayList();
463         ArrayList archList = new ArrayList();
464         ArrayList localeList = new ArrayList();
465         while ( rest.length() > 0 )
466         {
467             /* Must start with __ at this point */
468             if ( !rest.startsWith( "__" ) )
469             {
470                 return null;
471             }
472             rest = rest.substring( 2 );
473             // Get option and argument
474             char option = rest.charAt( 0 );
475             idx = rest.indexOf( "__" );
476             String arg = null;
477             if ( idx == -1 )
478             {
479                 arg = rest.substring( 1 );
480                 rest = "";
481             }
482             else
483             {
484                 arg = rest.substring( 1, idx );
485                 rest = rest.substring( idx );
486             }
487             switch ( option )
488             {
489                 case 'V':
490                     versionId = arg;
491                     break;
492                 case 'O':
493                     osList.add( arg );
494                     break;
495                 case 'A':
496                     archList.add( arg );
497                     break;
498                 case 'L':
499                     localeList.add( arg );
500                     break;
501                 default:
502                     return null; // error
503             }
504         }
505 
506         return new JnlpResource( _servletContext, name + extension, /* Resource name in URL request */
507                                  versionId, listToStrings( osList ), listToStrings( archList ),
508                                  listToStrings( localeList ), dir + filename, /* Resource name in WAR file */
509                                  versionId );
510     }
511 
512     private String[] listToStrings( List list )
513     {
514         if ( list.size() == 0 )
515         {
516             return null;
517         }
518         return (String[]) list.toArray( new String[list.size()] );
519     }
520 
521     // Returns false if parsing failed
522     private void parseVersionXML( final List versionList, final List platformList, final String dir,
523                                   final JnlpResource versionRes )
524     {
525         if ( !versionRes.exists() )
526         {
527             return;
528         }
529 
530         // Parse XML into a more understandable format
531         XMLNode root = null;
532         try
533         {
534             DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
535             DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
536             Document doc = docBuilder.parse( new BufferedInputStream( versionRes.getResource().openStream() ) );
537             doc.getDocumentElement().normalize();
538 
539             // Convert document into an XMLNode structure, since we already got utility methods
540             //  to handle these. We should really use the data-binding stuff here - but that will come
541             //  later
542             //
543             root = XMLParsing.convert( doc.getDocumentElement() );
544         }
545         catch ( SAXParseException err )
546         {
547             _log.addWarning( "servlet.log.warning.xml.parsing", versionRes.getPath(),
548                              Integer.toString( err.getLineNumber() ), err.getMessage() );
549             return;
550         }
551         catch ( Throwable t )
552         {
553             _log.addWarning( "servlet.log.warning.xml.reading", versionRes.getPath(), t );
554             return;
555         }
556 
557         // Check that root element is a <jnlp> tag
558         if ( !root.getName().equals( "jnlp-versions" ) )
559         {
560             _log.addWarning( "servlet.log.warning.xml.missing-jnlp", versionRes.getPath() );
561             return;
562         }
563 
564         // Visit all <resource> elements
565         XMLParsing.visitElements( root, "<resource>", new XMLParsing.ElementVisitor()
566         {
567             public void visitElement( XMLNode node )
568             {
569                 XMLNode pattern = XMLParsing.findElementPath( node, "<pattern>" );
570                 if ( pattern == null )
571                 {
572                     _log.addWarning( "servlet.log.warning.xml.missing-pattern", versionRes.getPath() );
573                 }
574                 else
575                 {
576                     // Parse pattern
577                     String name = XMLParsing.getElementContent( pattern, "<name>", "" );
578                     String versionId = XMLParsing.getElementContent( pattern, "<version-id>" );
579                     String[] os = XMLParsing.getMultiElementContent( pattern, "<os>" );
580                     String[] arch = XMLParsing.getMultiElementContent( pattern, "<arch>" );
581                     String[] locale = XMLParsing.getMultiElementContent( pattern, "<locale>" );
582                     // Get return request
583                     String file = XMLParsing.getElementContent( node, "<file>" );
584                     if ( versionId == null || file == null )
585                     {
586                         _log.addWarning( "servlet.log.warning.xml.missing-elems", versionRes.getPath() );
587                     }
588                     else
589                     {
590                         JnlpResource res =
591                             new JnlpResource( _servletContext, name, versionId, os, arch, locale, dir + file,
592                                               versionId );
593                         if ( res.exists() )
594                         {
595                             versionList.add( res );
596                             if ( _log.isDebugLevel() )
597                             {
598                                 _log.addDebug( "Read resource: " + res );
599                             }
600                         }
601                         else
602                         {
603                             _log.addWarning( "servlet.log.warning.missing-file", file, versionRes.getPath() );
604                         }
605                     }
606                 }
607             }
608         } );
609 
610         // Visit all <resource> elements
611         XMLParsing.visitElements( root, "<platform>", new XMLParsing.ElementVisitor()
612         {
613             public void visitElement( XMLNode node )
614             {
615                 XMLNode pattern = XMLParsing.findElementPath( node, "<pattern>" );
616                 if ( pattern == null )
617                 {
618                     _log.addWarning( "servlet.log.warning.xml.missing-pattern", versionRes.getPath() );
619                 }
620                 else
621                 {
622                     // Parse pattern
623                     String name = XMLParsing.getElementContent( pattern, "<name>", "" );
624                     String versionId = XMLParsing.getElementContent( pattern, "<version-id>" );
625                     String[] os = XMLParsing.getMultiElementContent( pattern, "<os>" );
626                     String[] arch = XMLParsing.getMultiElementContent( pattern, "<arch>" );
627                     String[] locale = XMLParsing.getMultiElementContent( pattern, "<locale>" );
628                     // Get return request
629                     String file = XMLParsing.getElementContent( node, "<file>" );
630                     String productId = XMLParsing.getElementContent( node, "<product-version-id>" );
631 
632                     if ( versionId == null || file == null || productId == null )
633                     {
634                         _log.addWarning( "servlet.log.warning.xml.missing-elems2", versionRes.getPath() );
635                     }
636                     else
637                     {
638                         JnlpResource res =
639                             new JnlpResource( _servletContext, name, versionId, os, arch, locale, dir + file,
640                                               productId );
641                         if ( res.exists() )
642                         {
643                             platformList.add( res );
644                             if ( _log.isDebugLevel() )
645                             {
646                                 _log.addDebug( "Read platform resource: " + res );
647                             }
648                         }
649                         else
650                         {
651                             _log.addWarning( "servlet.log.warning.missing-file", file, versionRes.getPath() );
652                         }
653                     }
654                 }
655             }
656         } );
657     }
658 }
659 
660