FELIX-1910 Refactored LicenseServlet to use regular URLs to address resources instead of request parameters; ensure bundles are displayed in bundle-name sort order (as intended)

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@912361 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/LicenseServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/LicenseServlet.java
index 8aa4d17..27295e3 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/LicenseServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/LicenseServlet.java
@@ -36,6 +36,7 @@
 import org.apache.felix.webconsole.WebConsoleUtil;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.osgi.framework.Bundle;
@@ -44,18 +45,18 @@
 /**
  * LicenseServlet provides the licenses plugin that browses through the bundles,
  * searching for common license files.
- * 
+ *
  * TODO: add support for 'Bundle-License' manifest header
  */
 public final class LicenseServlet extends SimpleWebConsolePlugin implements OsgiManagerPlugin
 {
     // common names (without extension) of the license files.
-    private static final String LICENSE_FILES[] =  { "README", "DISCLAIMER", "LICENSE", "NOTICE" };
-    
+    static final String LICENSE_FILES[] =  { "README", "DISCLAIMER", "LICENSE", "NOTICE" };
+
     static final String LABEL = "licenses";
     static final String TITLE = "Licenses";
     static final String CSS[] = { "/res/ui/license.css" };
-    
+
     // templates
     private final String TEMPLATE;
 
@@ -65,7 +66,7 @@
     public LicenseServlet()
     {
         super(LABEL, TITLE, CSS);
-        
+
         // load templates
         TEMPLATE = readTemplateFile( "/templates/license.html" );
     }
@@ -76,91 +77,21 @@
     protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException
     {
-        final String bid = request.getParameter("bid");
-
-        if (bid != null)
+        final PathInfo pathInfo = PathInfo.parse( request.getPathInfo() );
+        if ( pathInfo != null )
         {
-            Bundle bundle = getBundleContext().getBundle(Long.parseLong(bid));
-
-            // Check bundle
-            if (bundle == null)
+            if ( !sendResource( pathInfo, response ) )
             {
-                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-                    "No bundle with ID " + bid);
-                return;
+                response.sendError( HttpServletResponse.SC_NOT_FOUND, "Cannot send data .." );
             }
-
-            // Check if URL is given and *validate* if it is a license file.
-            // Otherwise, using this servlet, an intruder can read ANY file in the bundle
-            final String url = request.getParameter("url"); // file location
-            if (url == null)
-            {
-                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-                    "Missing parameter 'url'");
-                return;
-            }
-
-            String name = url.substring(url.lastIndexOf('/') + 1);
-            boolean isLicense = false;
-            for (int i = 0; !isLicense && i < LICENSE_FILES.length; i++)
-            {
-                isLicense = name.startsWith(LICENSE_FILES[i]);
-            }
-            if (!isLicense)
-            {
-                response.sendError(HttpServletResponse.SC_FORBIDDEN,
-                    "Requested non-license file, go away!");
-                return;
-            }
-
-            final String jar = request.getParameter("jar"); // inner Jar file
-            response.setContentType("text/plain");
-
-            if (jar == null)
-            {
-                InputStream input = bundle.getResource(url).openStream();
-                try
-                {
-                    IOUtils.copy(input, response.getWriter());
-                }
-                finally
-                {
-                    IOUtils.closeQuietly(input);
-                }
-            }
-            else
-            { // license is in a nested JAR
-                ZipInputStream zin = null;
-                InputStream input = bundle.getResource(jar).openStream();
-                try
-                {
-                    zin = new ZipInputStream(input);
-                    for (ZipEntry zentry = zin.getNextEntry(); zentry != null; zentry = zin.getNextEntry())
-                    {
-                        if (url.equals(zentry.getName()))
-                        {
-                            IOUtils.copy(zin, response.getWriter());
-                            return;
-                        }
-                    }
-                }
-                finally
-                {
-
-                    IOUtils.closeQuietly(zin);
-                    IOUtils.closeQuietly(input);
-                }
-
-                throw new ServletException("License file:" + url + " not found!");
-            }
-
         }
         else
         {
-            super.doGet(request, response);
+            super.doGet( request, response );
         }
     }
 
+
     /**
      * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
      */
@@ -175,10 +106,10 @@
 
         res.getWriter().print(TEMPLATE);
     }
-    
-    private static final JSONObject getBundleData(Bundle[] bundles) throws IOException
+
+    private static final JSONArray getBundleData(Bundle[] bundles) throws IOException
     {
-        JSONObject ret = new JSONObject();
+        JSONArray ret = new JSONArray();
         try
         {
             for (int i = 0; i < bundles.length; i++)
@@ -189,10 +120,10 @@
                 if (files.length() > 0)
                 { // has resources
                     JSONObject data = new JSONObject();
-                    data.put("bid", bundle.getBundleId());
-                    data.put("title", Util.getName(bundle));
-                    data.put("files", files);
-                    ret.put(String.valueOf(bundle.getBundleId()), data);
+                    data.put( "bid", bundle.getBundleId() );
+                    data.put( "title", Util.getName( bundle ) );
+                    data.put( "files", files );
+                    ret.put( data );
                 }
             }
         }
@@ -209,61 +140,60 @@
         return path.substring( path.lastIndexOf( '/' ) + 1 );
     }
 
-    private static final JSONObject findResource( Bundle bundle, String[] patterns )
-        throws IOException, JSONException
-    {
 
+    private static final JSONObject findResource( Bundle bundle, String[] patterns ) throws IOException, JSONException
+    {
         JSONObject ret = new JSONObject();
 
-        for ( int i = 0; i < patterns.length; i++) 
+        for ( int i = 0; i < patterns.length; i++ )
         {
-            Enumeration entries = bundle.findEntries( "/", patterns[i] + "*", true);
+            Enumeration entries = bundle.findEntries( "/", patterns[i] + "*", true );
             if ( entries != null )
             {
                 while ( entries.hasMoreElements() )
                 {
-                    URL url = (URL) entries.nextElement();
+                    URL url = ( URL ) entries.nextElement();
                     JSONObject entry = new JSONObject();
                     entry.put( "path", url.getPath() );
-                    entry.put( "url", getName(url.getPath()) );
+                    entry.put( "url", getName( url.getPath() ) );
                     ret.append( "__res__", entry );
                 }
             }
         }
 
-        Enumeration entries = bundle.findEntries("/", "*.jar", true);
+        Enumeration entries = bundle.findEntries( "/", "*.jar", true );
         if ( entries != null )
         {
             while ( entries.hasMoreElements() )
             {
-                URL url = (URL) entries.nextElement();
+                URL url = ( URL ) entries.nextElement();
                 final String resName = getName( url.getPath() );
 
                 InputStream ins = null;
                 try
                 {
                     ins = url.openStream();
-                    ZipInputStream zin = new ZipInputStream(ins);
+                    ZipInputStream zin = new ZipInputStream( ins );
                     for ( ZipEntry zentry = zin.getNextEntry(); zentry != null; zentry = zin.getNextEntry() )
                     {
                         String name = zentry.getName();
 
                         // ignore directory entries
-                        if ( name.endsWith("/") )
+                        if ( name.endsWith( "/" ) )
                         {
                             continue;
                         }
 
                         // cut off path and use file name for checking against patterns
-                        name = name.substring(name.lastIndexOf('/') + 1);
+                        name = name.substring( name.lastIndexOf( '/' ) + 1 );
                         for ( int i = 0; i < patterns.length; i++ )
                         {
-                            if ( name.startsWith(patterns[i]) )
+                            if ( name.startsWith( patterns[i] ) )
                             {
                                 JSONObject entry = new JSONObject();
                                 entry.put( "jar", url.getPath() );
                                 entry.put( "path", zentry.getName() );
-                                entry.put( "url", getName(name) );
+                                entry.put( "url", getName( name ) );
                                 ret.append( resName, entry );
                             }
                         }
@@ -271,7 +201,7 @@
                 }
                 finally
                 {
-                    IOUtils.closeQuietly(ins);
+                    IOUtils.closeQuietly( ins );
                 }
 
             }
@@ -280,4 +210,133 @@
         return ret;
     }
 
+
+    private boolean sendResource( final PathInfo pathInfo, final HttpServletResponse response ) throws IOException
+    {
+
+        final String name = pathInfo.licenseFile.substring( pathInfo.licenseFile.lastIndexOf( '/' ) + 1 );
+        boolean isLicense = false;
+        for ( int i = 0; !isLicense && i < LICENSE_FILES.length; i++ )
+        {
+            isLicense = name.startsWith( LICENSE_FILES[i] );
+        }
+
+        final Bundle bundle = getBundleContext().getBundle( pathInfo.bundleId );
+        if ( bundle == null )
+        {
+            return false;
+        }
+
+        if ( pathInfo.innerJar == null )
+        {
+            final URL resource = bundle.getResource( pathInfo.licenseFile );
+            if ( resource != null )
+            {
+                final InputStream input = resource.openStream();
+                try
+                {
+                    IOUtils.copy( input, response.getWriter() );
+                    return true;
+                }
+                finally
+                {
+                    IOUtils.closeQuietly( input );
+                }
+            }
+        }
+
+        // license is in a nested JAR
+        final URL zipResource = bundle.getResource( pathInfo.innerJar );
+        if ( zipResource != null )
+        {
+            final InputStream input = zipResource.openStream();
+            ZipInputStream zin = null;
+            try
+            {
+                zin = new ZipInputStream( input );
+                for ( ZipEntry zentry = zin.getNextEntry(); zentry != null; zentry = zin.getNextEntry() )
+                {
+                    if ( pathInfo.licenseFile.equals( zentry.getName() ) )
+                    {
+                        IOUtils.copy( zin, response.getWriter() );
+                        return true;
+                    }
+                }
+            }
+            finally
+            {
+
+                IOUtils.closeQuietly( zin );
+                IOUtils.closeQuietly( input );
+            }
+        }
+
+        // throw new ServletException("License file:" + url + " not found!");
+        return false;
+    }
+
+    // package private for unit testing of the parse method
+    static class PathInfo
+    {
+        final long bundleId;
+        final String innerJar;
+        final String licenseFile;
+
+
+        static PathInfo parse( final String pathInfo )
+        {
+            if ( pathInfo == null || pathInfo.length() == 0 || !pathInfo.startsWith( "/" + LABEL + "/" ) )
+            {
+                return null;
+            }
+
+            // cut off label prefix including slashes around the label
+            final String parts = pathInfo.substring( LABEL.length() + 2 );
+
+            int slash = parts.indexOf( '/' );
+            if ( slash <= 0 )
+            {
+                return null;
+            }
+
+            final long bundleId;
+            try
+            {
+                bundleId = Long.parseLong( parts.substring( 0, slash ) );
+                if ( bundleId < 0 )
+                {
+                    return null;
+                }
+            }
+            catch ( NumberFormatException nfe )
+            {
+                // illegal bundle id
+                return null;
+            }
+
+            final String innerJar;
+            int jarSep = parts.indexOf( "!/", slash );
+            if ( jarSep < 0 )
+            {
+                innerJar = null;
+            }
+            else
+            {
+                innerJar = parts.substring( slash, jarSep );
+                slash = jarSep + 2; // ignore bang-slash
+            }
+
+            final String licenseFile = parts.substring( slash );
+
+            return new PathInfo( bundleId, innerJar, licenseFile );
+        }
+
+
+        private PathInfo( final long bundleId, final String innerJar, final String licenseFile )
+        {
+            this.bundleId = bundleId;
+            this.innerJar = innerJar;
+            this.licenseFile = licenseFile;
+        }
+    }
 }
diff --git a/webconsole/src/main/resources/res/ui/license.js b/webconsole/src/main/resources/res/ui/license.js
index f378161..a4aa3a8 100644
--- a/webconsole/src/main/resources/res/ui/license.js
+++ b/webconsole/src/main/resources/res/ui/license.js
@@ -18,9 +18,9 @@
 var licenseButtons = false;
 var licenseDetails = false;
 
-function displayBundle(/* String */ bundleId)
+function displayBundle(/* String */ bundleIndex)
 {
-    var theBundleData = bundleData[bundleId];
+    var theBundleData = bundleData[bundleIndex];
     if (!theBundleData)
     {
         return;
@@ -39,10 +39,18 @@
             for (var idx in entry)
             {
                 var descr = entry[idx];
-				var jar = descr.jar ? '&jar=' + descr.jar : ''; // inner jar attribute
-				var link = pluginRoot + '?bid=' + bundleId + '&url=' + descr.path + jar;
+
+                var link = pluginRoot + "/" + theBundleData.bid;
+                if (descr.jar)
+                {
+                    link += descr.jar + "!/"; // inner jar attribute
+                }
+                link += descr.path;
+
 				buttons += '<a href="' + link + '">' + descr.url + '</a> ';
-				if (!firstPage) {
+
+				if (!firstPage)
+				{
 				    firstPage = link;
 				}
             }
@@ -64,7 +72,7 @@
     }
     
 	$("#licenseLeft a").removeClass('ui-state-default ui-corner-all');
-	$("#licenseLeft #" +bundleId).addClass('ui-state-default ui-corner-all');
+	$("#licenseLeft #" +bundleIndex).addClass('ui-state-default ui-corner-all');
 
     $('#licenseButtons a').click(function() {
        licenseDetails.load(this.href);
@@ -81,7 +89,7 @@
 	// render list of bundles
 	var txt = "";
 	for(id in bundleData) {
-		txt += '<a id="' + id + '" href="javascript:displayBundle(\'' + id + '\')">' + bundleData[id]['title'] + '</a>';
+		txt += '<a id="' + id + '" href="javascript:displayBundle(\'' + id + '\')">' + bundleData[id].title + '</a>';
 	}
 	if (txt) {
 		$("#licenseLeft").html(txt);
@@ -90,5 +98,5 @@
 	}
 
 	// display first element
-	for(i in bundleData) {displayBundle(i);break;}
+	displayBundle(0);
 });
\ No newline at end of file
diff --git a/webconsole/src/test/java/org/apache/felix/webconsole/internal/misc/LicenseServletTest.java b/webconsole/src/test/java/org/apache/felix/webconsole/internal/misc/LicenseServletTest.java
new file mode 100644
index 0000000..48a9f15
--- /dev/null
+++ b/webconsole/src/test/java/org/apache/felix/webconsole/internal/misc/LicenseServletTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.webconsole.internal.misc;
+
+
+import junit.framework.TestCase;
+
+
+public class LicenseServletTest extends TestCase
+{
+    // required prefix of a valid path to parse with PathInfo.parse
+    private static final String PREFIX = "/" + LicenseServlet.LABEL + "/";
+
+
+    public void test_PathInfo_parse_null()
+    {
+        // if the path is empty, null or not starting with /LABEL/
+        assertNull( LicenseServlet.PathInfo.parse( null ) );
+        assertNull( LicenseServlet.PathInfo.parse( "" ) );
+        assertNull( LicenseServlet.PathInfo.parse( LicenseServlet.LABEL ) );
+        assertNull( LicenseServlet.PathInfo.parse( LicenseServlet.LABEL + "x" ) );
+        assertNull( LicenseServlet.PathInfo.parse( LicenseServlet.LABEL + "/" ) );
+        assertNull( LicenseServlet.PathInfo.parse( "/any_not_label/" ) );
+
+        // if the path is only the label (with or with trailing slash)
+        assertNull( LicenseServlet.PathInfo.parse( "/" + LicenseServlet.LABEL ) );
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX ) );
+
+        // if path has second part not followed by a slash
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "xyz" ) );
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "-5" ) );
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "5.5" ) );
+
+        // if path has second part not converting to a positive long
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "xyz/trailing" ) );
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "-5/trailing" ) );
+        assertNull( LicenseServlet.PathInfo.parse( PREFIX + "5.5/trailing" ) );
+    }
+
+
+    public void test_PathInfo_parse_direct()
+    {
+        final long bundleId = 5;
+        final String licenseFile = "/META-INF/LICENSE";
+        LicenseServlet.PathInfo pi = LicenseServlet.PathInfo.parse( PREFIX + bundleId + licenseFile );
+        assertNotNull( pi );
+        assertEquals( bundleId, pi.bundleId );
+        assertNull( pi.innerJar );
+        assertEquals( licenseFile, pi.licenseFile );
+    }
+
+
+    public void test_PathInfo_parse_embedded()
+    {
+        final long bundleId = 5;
+        final String innerJar = "/some.jar";
+        final String licenseFile = "META-INF/LICENSE";
+        LicenseServlet.PathInfo pi = LicenseServlet.PathInfo.parse( PREFIX + bundleId + innerJar + "!/" + licenseFile );
+        assertNotNull( pi );
+        assertEquals( bundleId, pi.bundleId );
+        assertEquals( innerJar, pi.innerJar );
+        assertEquals( licenseFile, pi.licenseFile );
+    }
+}