1//
2//  ========================================================================
3//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4//  ------------------------------------------------------------------------
5//  All rights reserved. This program and the accompanying materials
6//  are made available under the terms of the Eclipse Public License v1.0
7//  and Apache License v2.0 which accompanies this distribution.
8//
9//      The Eclipse Public License is available at
10//      http://www.eclipse.org/legal/epl-v10.html
11//
12//      The Apache License v2.0 is available at
13//      http://www.opensource.org/licenses/apache2.0.php
14//
15//  You may elect to redistribute this code under either of these licenses.
16//  ========================================================================
17//
18
19package org.eclipse.jetty.webapp;
20
21import java.io.File;
22import java.io.IOException;
23import java.net.URI;
24import java.net.URISyntaxException;
25import java.net.URL;
26import java.net.URLClassLoader;
27import java.util.ArrayList;
28import java.util.List;
29import java.util.Locale;
30import java.util.regex.Pattern;
31
32import org.eclipse.jetty.server.Connector;
33import org.eclipse.jetty.server.Server;
34import org.eclipse.jetty.util.IO;
35import org.eclipse.jetty.util.PatternMatcher;
36import org.eclipse.jetty.util.URIUtil;
37import org.eclipse.jetty.util.log.Log;
38import org.eclipse.jetty.util.log.Logger;
39import org.eclipse.jetty.util.resource.JarResource;
40import org.eclipse.jetty.util.resource.Resource;
41import org.eclipse.jetty.util.resource.ResourceCollection;
42
43public class WebInfConfiguration extends AbstractConfiguration
44{
45    private static final Logger LOG = Log.getLogger(WebInfConfiguration.class);
46
47    public static final String TEMPDIR_CONFIGURED = "org.eclipse.jetty.tmpdirConfigured";
48    public static final String CONTAINER_JAR_PATTERN = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
49    public static final String WEBINF_JAR_PATTERN = "org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern";
50
51    /**
52     * If set, to a list of URLs, these resources are added to the context
53     * resource base as a resource collection.
54     */
55    public static final String RESOURCE_URLS = "org.eclipse.jetty.resources";
56
57    protected Resource _preUnpackBaseResource;
58
59    @Override
60    public void preConfigure(final WebAppContext context) throws Exception
61    {
62        // Look for a work directory
63        File work = findWorkDirectory(context);
64        if (work != null)
65            makeTempDirectory(work, context, false);
66
67        //Make a temp directory for the webapp if one is not already set
68        resolveTempDirectory(context);
69
70        //Extract webapp if necessary
71        unpack (context);
72
73
74        //Apply an initial ordering to the jars which governs which will be scanned for META-INF
75        //info and annotations. The ordering is based on inclusion patterns.
76        String tmp = (String)context.getAttribute(WEBINF_JAR_PATTERN);
77        Pattern webInfPattern = (tmp==null?null:Pattern.compile(tmp));
78        tmp = (String)context.getAttribute(CONTAINER_JAR_PATTERN);
79        Pattern containerPattern = (tmp==null?null:Pattern.compile(tmp));
80
81        //Apply ordering to container jars - if no pattern is specified, we won't
82        //match any of the container jars
83        PatternMatcher containerJarNameMatcher = new PatternMatcher ()
84        {
85            public void matched(URI uri) throws Exception
86            {
87                context.getMetaData().addContainerJar(Resource.newResource(uri));
88            }
89        };
90        ClassLoader loader = null;
91        if (context.getClassLoader() != null)
92            loader = context.getClassLoader().getParent();
93
94        while (loader != null && (loader instanceof URLClassLoader))
95        {
96            URL[] urls = ((URLClassLoader)loader).getURLs();
97            if (urls != null)
98            {
99                URI[] containerUris = new URI[urls.length];
100                int i=0;
101                for (URL u : urls)
102                {
103                    try
104                    {
105                        containerUris[i] = u.toURI();
106                    }
107                    catch (URISyntaxException e)
108                    {
109                        containerUris[i] = new URI(u.toString().replaceAll(" ", "%20"));
110                    }
111                    i++;
112                }
113                containerJarNameMatcher.match(containerPattern, containerUris, false);
114            }
115            loader = loader.getParent();
116        }
117
118        //Apply ordering to WEB-INF/lib jars
119        PatternMatcher webInfJarNameMatcher = new PatternMatcher ()
120        {
121            @Override
122            public void matched(URI uri) throws Exception
123            {
124                context.getMetaData().addWebInfJar(Resource.newResource(uri));
125            }
126        };
127        List<Resource> jars = findJars(context);
128
129        //Convert to uris for matching
130        URI[] uris = null;
131        if (jars != null)
132        {
133            uris = new URI[jars.size()];
134            int i=0;
135            for (Resource r: jars)
136            {
137                uris[i++] = r.getURI();
138            }
139        }
140        webInfJarNameMatcher.match(webInfPattern, uris, true); //null is inclusive, no pattern == all jars match
141    }
142
143
144    @Override
145    public void configure(WebAppContext context) throws Exception
146    {
147        //cannot configure if the context is already started
148        if (context.isStarted())
149        {
150            if (LOG.isDebugEnabled())
151                LOG.debug("Cannot configure webapp "+context+" after it is started");
152            return;
153        }
154
155        Resource web_inf = context.getWebInf();
156
157        // Add WEB-INF classes and lib classpaths
158        if (web_inf != null && web_inf.isDirectory() && context.getClassLoader() instanceof WebAppClassLoader)
159        {
160            // Look for classes directory
161            Resource classes= web_inf.addPath("classes/");
162            if (classes.exists())
163                ((WebAppClassLoader)context.getClassLoader()).addClassPath(classes);
164
165            // Look for jars
166            Resource lib= web_inf.addPath("lib/");
167            if (lib.exists() || lib.isDirectory())
168                ((WebAppClassLoader)context.getClassLoader()).addJars(lib);
169        }
170
171        // Look for extra resource
172        @SuppressWarnings("unchecked")
173        List<Resource> resources = (List<Resource>)context.getAttribute(RESOURCE_URLS);
174        if (resources!=null)
175        {
176            Resource[] collection=new Resource[resources.size()+1];
177            int i=0;
178            collection[i++]=context.getBaseResource();
179            for (Resource resource : resources)
180                collection[i++]=resource;
181            context.setBaseResource(new ResourceCollection(collection));
182        }
183    }
184
185    @Override
186    public void deconfigure(WebAppContext context) throws Exception
187    {
188        // delete temp directory if we had to create it or if it isn't called work
189        Boolean tmpdirConfigured = (Boolean)context.getAttribute(TEMPDIR_CONFIGURED);
190
191        if (context.getTempDirectory()!=null && (tmpdirConfigured == null || !tmpdirConfigured.booleanValue()) && !isTempWorkDirectory(context.getTempDirectory()))
192        {
193            IO.delete(context.getTempDirectory());
194            context.setTempDirectory(null);
195
196            //clear out the context attributes for the tmp dir only if we had to
197            //create the tmp dir
198            context.setAttribute(TEMPDIR_CONFIGURED, null);
199            context.setAttribute(WebAppContext.TEMPDIR, null);
200        }
201
202
203        //reset the base resource back to what it was before we did any unpacking of resources
204        context.setBaseResource(_preUnpackBaseResource);
205    }
206
207    /* ------------------------------------------------------------ */
208    /**
209     * @see org.eclipse.jetty.webapp.AbstractConfiguration#cloneConfigure(org.eclipse.jetty.webapp.WebAppContext, org.eclipse.jetty.webapp.WebAppContext)
210     */
211    @Override
212    public void cloneConfigure(WebAppContext template, WebAppContext context) throws Exception
213    {
214        File tmpDir=File.createTempFile(WebInfConfiguration.getCanonicalNameForWebAppTmpDir(context),"",template.getTempDirectory().getParentFile());
215        if (tmpDir.exists())
216        {
217            IO.delete(tmpDir);
218        }
219        tmpDir.mkdir();
220        tmpDir.deleteOnExit();
221        context.setTempDirectory(tmpDir);
222    }
223
224
225    /* ------------------------------------------------------------ */
226    /**
227     * Get a temporary directory in which to unpack the war etc etc.
228     * The algorithm for determining this is to check these alternatives
229     * in the order shown:
230     *
231     * <p>A. Try to use an explicit directory specifically for this webapp:</p>
232     * <ol>
233     * <li>
234     * Iff an explicit directory is set for this webapp, use it. Do NOT set
235     * delete on exit.
236     * </li>
237     * <li>
238     * Iff javax.servlet.context.tempdir context attribute is set for
239     * this webapp && exists && writeable, then use it. Do NOT set delete on exit.
240     * </li>
241     * </ol>
242     *
243     * <p>B. Create a directory based on global settings. The new directory
244     * will be called "Jetty_"+host+"_"+port+"__"+context+"_"+virtualhost
245     * Work out where to create this directory:
246     * <ol>
247     * <li>
248     * Iff $(jetty.home)/work exists create the directory there. Do NOT
249     * set delete on exit. Do NOT delete contents if dir already exists.
250     * </li>
251     * <li>
252     * Iff WEB-INF/work exists create the directory there. Do NOT set
253     * delete on exit. Do NOT delete contents if dir already exists.
254     * </li>
255     * <li>
256     * Else create dir in $(java.io.tmpdir). Set delete on exit. Delete
257     * contents if dir already exists.
258     * </li>
259     * </ol>
260     */
261    public void resolveTempDirectory (WebAppContext context)
262    {
263        //If a tmp directory is already set, we're done
264        File tmpDir = context.getTempDirectory();
265        if (tmpDir != null && tmpDir.isDirectory() && tmpDir.canWrite())
266        {
267            context.setAttribute(TEMPDIR_CONFIGURED, Boolean.TRUE);
268            return; // Already have a suitable tmp dir configured
269        }
270
271
272        // No temp directory configured, try to establish one.
273        // First we check the context specific, javax.servlet specified, temp directory attribute
274        File servletTmpDir = asFile(context.getAttribute(WebAppContext.TEMPDIR));
275        if (servletTmpDir != null && servletTmpDir.isDirectory() && servletTmpDir.canWrite())
276        {
277            // Use as tmpDir
278            tmpDir = servletTmpDir;
279            // Ensure Attribute has File object
280            context.setAttribute(WebAppContext.TEMPDIR,tmpDir);
281            // Set as TempDir in context.
282            context.setTempDirectory(tmpDir);
283            return;
284        }
285
286        try
287        {
288            // Put the tmp dir in the work directory if we had one
289            File work =  new File(System.getProperty("jetty.home"),"work");
290            if (work.exists() && work.canWrite() && work.isDirectory())
291            {
292                makeTempDirectory(work, context, false); //make a tmp dir inside work, don't delete if it exists
293            }
294            else
295            {
296                File baseTemp = asFile(context.getAttribute(WebAppContext.BASETEMPDIR));
297                if (baseTemp != null && baseTemp.isDirectory() && baseTemp.canWrite())
298                {
299                    // Use baseTemp directory (allow the funky Jetty_0_0_0_0.. subdirectory logic to kick in
300                    makeTempDirectory(baseTemp,context,false);
301                }
302                else
303                {
304                    makeTempDirectory(new File(System.getProperty("java.io.tmpdir")),context,true); //make a tmpdir, delete if it already exists
305                }
306            }
307        }
308        catch(Exception e)
309        {
310            tmpDir=null;
311            LOG.ignore(e);
312        }
313
314        //Third ... Something went wrong trying to make the tmp directory, just make
315        //a jvm managed tmp directory
316        if (context.getTempDirectory() == null)
317        {
318            try
319            {
320                // Last resort
321                tmpDir=File.createTempFile("JettyContext","");
322                if (tmpDir.exists())
323                    IO.delete(tmpDir);
324                tmpDir.mkdir();
325                tmpDir.deleteOnExit();
326                context.setTempDirectory(tmpDir);
327            }
328            catch(IOException e)
329            {
330                tmpDir = null;
331                throw new IllegalStateException("Cannot create tmp dir in "+System.getProperty("java.io.tmpdir")+ " for context "+context,e);
332            }
333        }
334    }
335
336    /**
337     * Given an Object, return File reference for object.
338     * Typically used to convert anonymous Object from getAttribute() calls to a File object.
339     * @param fileattr the file attribute to analyze and return from (supports type File and type String, all others return null)
340     * @return the File object, null if null, or null if not a File or String
341     */
342    private File asFile(Object fileattr)
343    {
344        if (fileattr == null)
345        {
346            return null;
347        }
348        if (fileattr instanceof File)
349        {
350            return (File)fileattr;
351        }
352        if (fileattr instanceof String)
353        {
354            return new File((String)fileattr);
355        }
356        return null;
357    }
358
359
360
361    public void makeTempDirectory (File parent, WebAppContext context, boolean deleteExisting)
362    throws IOException
363    {
364        if (parent != null && parent.exists() && parent.canWrite() && parent.isDirectory())
365        {
366            String temp = getCanonicalNameForWebAppTmpDir(context);
367            File tmpDir = new File(parent,temp);
368
369            if (deleteExisting && tmpDir.exists())
370            {
371                if (!IO.delete(tmpDir))
372                {
373                    if(LOG.isDebugEnabled())LOG.debug("Failed to delete temp dir "+tmpDir);
374                }
375
376                //If we can't delete the existing tmp dir, create a new one
377                if (tmpDir.exists())
378                {
379                    String old=tmpDir.toString();
380                    tmpDir=File.createTempFile(temp+"_","");
381                    if (tmpDir.exists())
382                        IO.delete(tmpDir);
383                    LOG.warn("Can't reuse "+old+", using "+tmpDir);
384                }
385            }
386
387            if (!tmpDir.exists())
388                tmpDir.mkdir();
389
390            //If the parent is not a work directory
391            if (!isTempWorkDirectory(tmpDir))
392            {
393                tmpDir.deleteOnExit();
394            }
395
396            if(LOG.isDebugEnabled())
397                LOG.debug("Set temp dir "+tmpDir);
398            context.setTempDirectory(tmpDir);
399        }
400    }
401
402
403    public void unpack (WebAppContext context) throws IOException
404    {
405        Resource web_app = context.getBaseResource();
406        _preUnpackBaseResource = context.getBaseResource();
407
408        if (web_app == null)
409        {
410            String war = context.getWar();
411            if (war!=null && war.length()>0)
412                web_app = context.newResource(war);
413            else
414                web_app=context.getBaseResource();
415
416            // Accept aliases for WAR files
417            if (web_app.getAlias() != null)
418            {
419                LOG.debug(web_app + " anti-aliased to " + web_app.getAlias());
420                web_app = context.newResource(web_app.getAlias());
421            }
422
423            if (LOG.isDebugEnabled())
424                LOG.debug("Try webapp=" + web_app + ", exists=" + web_app.exists() + ", directory=" + web_app.isDirectory()+" file="+(web_app.getFile()));
425            // Is the WAR usable directly?
426            if (web_app.exists() && !web_app.isDirectory() && !web_app.toString().startsWith("jar:"))
427            {
428                // No - then lets see if it can be turned into a jar URL.
429                Resource jarWebApp = JarResource.newJarResource(web_app);
430                if (jarWebApp.exists() && jarWebApp.isDirectory())
431                    web_app= jarWebApp;
432            }
433
434            // If we should extract or the URL is still not usable
435            if (web_app.exists()  && (
436                    (context.isCopyWebDir() && web_app.getFile() != null && web_app.getFile().isDirectory()) ||
437                    (context.isExtractWAR() && web_app.getFile() != null && !web_app.getFile().isDirectory()) ||
438                    (context.isExtractWAR() && web_app.getFile() == null) ||
439                    !web_app.isDirectory())
440                            )
441            {
442                // Look for sibling directory.
443                File extractedWebAppDir = null;
444
445                if (war!=null)
446                {
447                    // look for a sibling like "foo/" to a "foo.war"
448                    File warfile=Resource.newResource(war).getFile();
449                    if (warfile!=null && warfile.getName().toLowerCase(Locale.ENGLISH).endsWith(".war"))
450                    {
451                        File sibling = new File(warfile.getParent(),warfile.getName().substring(0,warfile.getName().length()-4));
452                        if (sibling.exists() && sibling.isDirectory() && sibling.canWrite())
453                            extractedWebAppDir=sibling;
454                    }
455                }
456
457                if (extractedWebAppDir==null)
458                    // Then extract it if necessary to the temporary location
459                    extractedWebAppDir= new File(context.getTempDirectory(), "webapp");
460
461                if (web_app.getFile()!=null && web_app.getFile().isDirectory())
462                {
463                    // Copy directory
464                    LOG.info("Copy " + web_app + " to " + extractedWebAppDir);
465                    web_app.copyTo(extractedWebAppDir);
466                }
467                else
468                {
469                    //Use a sentinel file that will exist only whilst the extraction is taking place.
470                    //This will help us detect interrupted extractions.
471                    File extractionLock = new File (context.getTempDirectory(), ".extract_lock");
472
473                    if (!extractedWebAppDir.exists())
474                    {
475                        //it hasn't been extracted before so extract it
476                        extractionLock.createNewFile();
477                        extractedWebAppDir.mkdir();
478                        LOG.info("Extract " + web_app + " to " + extractedWebAppDir);
479                        Resource jar_web_app = JarResource.newJarResource(web_app);
480                        jar_web_app.copyTo(extractedWebAppDir);
481                        extractionLock.delete();
482                    }
483                    else
484                    {
485                        //only extract if the war file is newer, or a .extract_lock file is left behind meaning a possible partial extraction
486                        if (web_app.lastModified() > extractedWebAppDir.lastModified() || extractionLock.exists())
487                        {
488                            extractionLock.createNewFile();
489                            IO.delete(extractedWebAppDir);
490                            extractedWebAppDir.mkdir();
491                            LOG.info("Extract " + web_app + " to " + extractedWebAppDir);
492                            Resource jar_web_app = JarResource.newJarResource(web_app);
493                            jar_web_app.copyTo(extractedWebAppDir);
494                            extractionLock.delete();
495                        }
496                    }
497                }
498                web_app = Resource.newResource(extractedWebAppDir.getCanonicalPath());
499            }
500
501            // Now do we have something usable?
502            if (!web_app.exists() || !web_app.isDirectory())
503            {
504                LOG.warn("Web application not found " + war);
505                throw new java.io.FileNotFoundException(war);
506            }
507
508            context.setBaseResource(web_app);
509
510            if (LOG.isDebugEnabled())
511                LOG.debug("webapp=" + web_app);
512        }
513
514
515        // Do we need to extract WEB-INF/lib?
516        if (context.isCopyWebInf() && !context.isCopyWebDir())
517        {
518            Resource web_inf= web_app.addPath("WEB-INF/");
519
520            File extractedWebInfDir= new File(context.getTempDirectory(), "webinf");
521            if (extractedWebInfDir.exists())
522                IO.delete(extractedWebInfDir);
523            extractedWebInfDir.mkdir();
524            Resource web_inf_lib = web_inf.addPath("lib/");
525            File webInfDir=new File(extractedWebInfDir,"WEB-INF");
526            webInfDir.mkdir();
527
528            if (web_inf_lib.exists())
529            {
530                File webInfLibDir = new File(webInfDir, "lib");
531                if (webInfLibDir.exists())
532                    IO.delete(webInfLibDir);
533                webInfLibDir.mkdir();
534
535                LOG.info("Copying WEB-INF/lib " + web_inf_lib + " to " + webInfLibDir);
536                web_inf_lib.copyTo(webInfLibDir);
537            }
538
539            Resource web_inf_classes = web_inf.addPath("classes/");
540            if (web_inf_classes.exists())
541            {
542                File webInfClassesDir = new File(webInfDir, "classes");
543                if (webInfClassesDir.exists())
544                    IO.delete(webInfClassesDir);
545                webInfClassesDir.mkdir();
546                LOG.info("Copying WEB-INF/classes from "+web_inf_classes+" to "+webInfClassesDir.getAbsolutePath());
547                web_inf_classes.copyTo(webInfClassesDir);
548            }
549
550            web_inf=Resource.newResource(extractedWebInfDir.getCanonicalPath());
551
552            ResourceCollection rc = new ResourceCollection(web_inf,web_app);
553
554            if (LOG.isDebugEnabled())
555                LOG.debug("context.resourcebase = "+rc);
556
557            context.setBaseResource(rc);
558        }
559    }
560
561
562    public File findWorkDirectory (WebAppContext context) throws IOException
563    {
564        if (context.getBaseResource() != null)
565        {
566            Resource web_inf = context.getWebInf();
567            if (web_inf !=null && web_inf.exists())
568            {
569               return new File(web_inf.getFile(),"work");
570            }
571        }
572        return null;
573    }
574
575
576    /**
577     * Check if the tmpDir itself is called "work", or if the tmpDir
578     * is in a directory called "work".
579     * @return true if File is a temporary or work directory
580     */
581    public boolean isTempWorkDirectory (File tmpDir)
582    {
583        if (tmpDir == null)
584            return false;
585        if (tmpDir.getName().equalsIgnoreCase("work"))
586            return true;
587        File t = tmpDir.getParentFile();
588        if (t == null)
589            return false;
590        return (t.getName().equalsIgnoreCase("work"));
591    }
592
593
594    /**
595     * Create a canonical name for a webapp temp directory.
596     * The form of the name is:
597     *  <code>"Jetty_"+host+"_"+port+"__"+resourceBase+"_"+context+"_"+virtualhost+base36_hashcode_of_whole_string</code>
598     *
599     *  host and port uniquely identify the server
600     *  context and virtual host uniquely identify the webapp
601     * @return the canonical name for the webapp temp directory
602     */
603    public static String getCanonicalNameForWebAppTmpDir (WebAppContext context)
604    {
605        StringBuffer canonicalName = new StringBuffer();
606        canonicalName.append("jetty-");
607
608        //get the host and the port from the first connector
609        Server server=context.getServer();
610        if (server!=null)
611        {
612            Connector[] connectors = context.getServer().getConnectors();
613
614            if (connectors.length>0)
615            {
616                //Get the host
617                String host = (connectors==null||connectors[0]==null?"":connectors[0].getHost());
618                if (host == null)
619                    host = "0.0.0.0";
620                canonicalName.append(host);
621
622                //Get the port
623                canonicalName.append("-");
624                //try getting the real port being listened on
625                int port = (connectors==null||connectors[0]==null?0:connectors[0].getLocalPort());
626                //if not available (eg no connectors or connector not started),
627                //try getting one that was configured.
628                if (port < 0)
629                    port = connectors[0].getPort();
630                canonicalName.append(port);
631                canonicalName.append("-");
632            }
633        }
634
635
636        //Resource  base
637        try
638        {
639            Resource resource = context.getBaseResource();
640            if (resource == null)
641            {
642                if (context.getWar()==null || context.getWar().length()==0)
643                    resource=context.newResource(context.getResourceBase());
644
645                // Set dir or WAR
646                resource = context.newResource(context.getWar());
647            }
648
649            String tmp = URIUtil.decodePath(resource.getURL().getPath());
650            if (tmp.endsWith("/"))
651                tmp = tmp.substring(0, tmp.length()-1);
652            if (tmp.endsWith("!"))
653                tmp = tmp.substring(0, tmp.length() -1);
654            //get just the last part which is the filename
655            int i = tmp.lastIndexOf("/");
656            canonicalName.append(tmp.substring(i+1, tmp.length()));
657            canonicalName.append("-");
658        }
659        catch (Exception e)
660        {
661            LOG.warn("Can't generate resourceBase as part of webapp tmp dir name", e);
662        }
663
664        //Context name
665        String contextPath = context.getContextPath();
666        contextPath=contextPath.replace('/','_');
667        contextPath=contextPath.replace('\\','_');
668        canonicalName.append(contextPath);
669
670        //Virtual host (if there is one)
671        canonicalName.append("-");
672        String[] vhosts = context.getVirtualHosts();
673        if (vhosts == null || vhosts.length <= 0)
674            canonicalName.append("any");
675        else
676            canonicalName.append(vhosts[0]);
677
678        // sanitize
679        for (int i=0;i<canonicalName.length();i++)
680        {
681            char c=canonicalName.charAt(i);
682            if (!Character.isJavaIdentifierPart(c) && "-.".indexOf(c)<0)
683                canonicalName.setCharAt(i,'.');
684        }
685
686        canonicalName.append("-");
687        return canonicalName.toString();
688    }
689
690    /**
691     * Look for jars in WEB-INF/lib
692     * @param context
693     * @return the list of jar resources found within context
694     * @throws Exception
695     */
696    protected List<Resource> findJars (WebAppContext context)
697    throws Exception
698    {
699        List<Resource> jarResources = new ArrayList<Resource>();
700
701        Resource web_inf = context.getWebInf();
702        if (web_inf==null || !web_inf.exists())
703            return null;
704
705        Resource web_inf_lib = web_inf.addPath("/lib");
706
707
708        if (web_inf_lib.exists() && web_inf_lib.isDirectory())
709        {
710            String[] files=web_inf_lib.list();
711            for (int f=0;files!=null && f<files.length;f++)
712            {
713                try
714                {
715                    Resource file = web_inf_lib.addPath(files[f]);
716                    String fnlc = file.getName().toLowerCase(Locale.ENGLISH);
717                    int dot = fnlc.lastIndexOf('.');
718                    String extension = (dot < 0 ? null : fnlc.substring(dot));
719                    if (extension != null && (extension.equals(".jar") || extension.equals(".zip")))
720                    {
721                        jarResources.add(file);
722                    }
723                }
724                catch (Exception ex)
725                {
726                    LOG.warn(Log.EXCEPTION,ex);
727                }
728            }
729        }
730        return jarResources;
731    }
732}
733