FELIX-1988 Apply 16.license_plugin.patch by Valentin Valchev (thanks)
FELIX-1910 Refactor LicenseServlet to support on-demand loading of licenses (part of 16.license_plugin.patch)

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@911770 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 ef819bd..8aa4d17 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
@@ -19,263 +19,265 @@
 package org.apache.felix.webconsole.internal.misc;
 
 
-import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.io.Reader;
 import java.net.URL;
 import java.util.Enumeration;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.commons.io.IOUtils;
-import org.apache.felix.webconsole.AbstractWebConsolePlugin;
-import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.DefaultVariableResolver;
+import org.apache.felix.webconsole.SimpleWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleUtil;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
 import org.json.JSONException;
-import org.json.JSONWriter;
+import org.json.JSONObject;
 import org.osgi.framework.Bundle;
-import org.osgi.service.component.ComponentContext;
 
 
 /**
- * The <code>LicenseServlet</code> TODO
+ * LicenseServlet provides the licenses plugin that browses through the bundles,
+ * searching for common license files.
+ * 
+ * TODO: add support for 'Bundle-License' manifest header
  */
-public class LicenseServlet extends AbstractWebConsolePlugin implements OsgiManagerPlugin
+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 LABEL = "licenses";
+    static final String TITLE = "Licenses";
+    static final String CSS[] = { "/res/ui/license.css" };
+    
+    // templates
+    private final String TEMPLATE;
 
-    private static final String[] CSS_REFS =
-        { "res/ui/license.css" };
-
-
-    public String getLabel()
+    /**
+     * Default constructor
+     */
+    public LicenseServlet()
     {
-        return "licenses";
+        super(LABEL, TITLE, CSS);
+        
+        // load templates
+        TEMPLATE = readTemplateFile( "/templates/license.html" );
     }
 
-
-    public String getTitle()
+    /**
+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    protected void doGet(HttpServletRequest request, HttpServletResponse response)
+        throws ServletException, IOException
     {
-        return "Licenses";
+        final String bid = request.getParameter("bid");
+
+        if (bid != null)
+        {
+            Bundle bundle = getBundleContext().getBundle(Long.parseLong(bid));
+
+            // Check bundle
+            if (bundle == null)
+            {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                    "No bundle with ID " + bid);
+                return;
+            }
+
+            // 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);
+        }
     }
 
-
-    protected String[] getCssReferences()
+    /**
+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    protected void renderContent( HttpServletRequest request, HttpServletResponse res ) throws IOException
     {
-        return CSS_REFS;
-    }
-
-
-    protected void renderContent( HttpServletRequest req, HttpServletResponse res ) throws IOException
-    {
-        PrintWriter pw = res.getWriter();
-
-        final String appRoot = ( String ) req.getAttribute( WebConsoleConstants.ATTR_APP_ROOT );
-        Util.script( pw, appRoot, "license.js" );
-
         Bundle[] bundles = getBundleContext().getBundles();
         Util.sort( bundles );
 
-        Util.startScript( pw );
-        pw.print( "bundleData = " );
-        JSONWriter jw = new JSONWriter( pw );
+        // prepare variables
+        DefaultVariableResolver vars = ( ( DefaultVariableResolver ) WebConsoleUtil.getVariableResolver( request ) );
+        vars.put( "__data__", getBundleData(bundles).toString());
+
+        res.getWriter().print(TEMPLATE);
+    }
+    
+    private static final JSONObject getBundleData(Bundle[] bundles) throws IOException
+    {
+        JSONObject ret = new JSONObject();
         try
         {
-            jw.object();
-            for ( int i = 0; i < bundles.length; i++ )
+            for (int i = 0; i < bundles.length; i++)
             {
                 Bundle bundle = bundles[i];
-                jw.key( String.valueOf( bundle.getBundleId() ) );
 
-                jw.object();
-
-                jw.key( "title" );
-                jw.value( Util.getName( bundle ) );
-
-                jw.key( "files" );
-                jw.object();
-                findResource( jw, bundle, new String[]
-                    { "README", "DISCLAIMER", "LICENSE", "NOTICE" } );
-                jw.endObject();
-
-                jw.endObject();
+                JSONObject files = findResource(bundle, LICENSE_FILES);
+                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);
+                }
             }
-            jw.endObject();
-            pw.println( ";" );
         }
-        catch ( JSONException je )
+        catch (JSONException je)
         {
-            throw new IOException( je.toString() );
+            throw new IOException(je.toString());
         }
-        Util.endScript( pw );
-
-        pw.println( "<div id='licenseContent'>" );
-
-        pw.println( "<div id='licenseLeft'>" );
-        for ( int i = 0; i < bundles.length; i++ )
-        {
-            Bundle bundle = bundles[i];
-            String link = "displayBundle( \"" + bundle.getBundleId() + "\" );";
-            pw.println( "<a href='javascript:" + link + "'>" + Util.getName( bundle ) + "</a><br />" );
-
-        }
-        pw.println( "</div>" );
-
-        pw.println( "<div id='licenseRight'>" );
-        pw.println( "<div id='licenseButtons' class='licenseButtons'>&nbsp;</div>" );
-        pw.println( "<br />" );
-        pw.println( "<div id='licenseDetails' class='licenseDetails'>&nbsp;</div>" );
-        pw.println( "</div>" );
-
-        pw.println( "<div id='licenseClear'>&nbsp;</div>" );
-
-        pw.println( "</div>" ); // licenseContent
-
-        Util.startScript( pw );
-        pw.println( "displayBundle( '0' );" );
-        Util.endScript( pw );
+        return ret;
     }
 
 
-    private String getName( String path )
+    private static final String getName( String path )
     {
         return path.substring( path.lastIndexOf( '/' ) + 1 );
     }
 
-
-    private void findResource( JSONWriter jw, Bundle bundle, String[] patterns ) throws IOException, JSONException
+    private static final JSONObject findResource( Bundle bundle, String[] patterns )
+        throws IOException, JSONException
     {
-        jw.key( "Bundle Resources" ); // aka the bundle files
-        jw.array();
-        for ( int i = 0; i < patterns.length; i++ )
+
+        JSONObject ret = new JSONObject();
+
+        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();
-                    jw.object();
-                    jw.key( "url" );
-                    jw.value( getName( url.getPath() ) );
-                    jw.key( "data" );
-                    jw.value( readResource( url ) );
-                    jw.endObject();
+                    URL url = (URL) entries.nextElement();
+                    JSONObject entry = new JSONObject();
+                    entry.put( "path", url.getPath() );
+                    entry.put( "url", getName(url.getPath()) );
+                    ret.append( "__res__", entry );
                 }
             }
         }
-        jw.endArray();
 
-        Enumeration entries = bundle.findEntries( "/", "*.jar", true );
+        Enumeration entries = bundle.findEntries("/", "*.jar", true);
         if ( entries != null )
         {
             while ( entries.hasMoreElements() )
             {
-                URL url = ( URL ) entries.nextElement();
-
-                jw.key( "Embedded " + getName( url.getPath() ) );
-                jw.array();
+                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]) )
                             {
-                                jw.object();
-                                jw.key( "url" );
-                                jw.value( getName( name ) );
-                                jw.key( "data" );
-                                jw.value( readResource( new FilterInputStream( zin )
-                                {
-                                    public void close()
-                                    {
-                                        // nothing for now
-                                    }
-                                } ) );
-                                jw.endObject();
-                                break;
+                                JSONObject entry = new JSONObject();
+                                entry.put( "jar", url.getPath() );
+                                entry.put( "path", zentry.getName() );
+                                entry.put( "url", getName(name) );
+                                ret.append( resName, entry );
                             }
                         }
                     }
                 }
                 finally
                 {
-                    IOUtils.closeQuietly( ins );
+                    IOUtils.closeQuietly(ins);
                 }
 
-                jw.endArray();
-            }
-        }
-    }
-
-
-    private String getResource( Bundle bundle, String[] path ) throws IOException
-    {
-        for ( int i = 0; i < path.length; i++ )
-        {
-            URL resource = bundle.getResource( path[i] );
-            if ( resource != null )
-            {
-                return readResource( resource );
             }
         }
 
-        return null;
-    }
-
-
-    private String readResource( URL resource ) throws IOException
-    {
-        return readResource( resource.openStream() );
-    }
-
-
-    private String readResource( InputStream resource ) throws IOException
-    {
-        try
-        {
-            // return new String(IOUtils.toCharArray(resource, "ISO-8859-1"));
-            // the method below is faster that the one above
-            return new String(IOUtils.toByteArray(resource), "ISO-8859-1");
-        }
-        finally
-        {
-            IOUtils.closeQuietly(resource);
-        }
-    }
-
-
-    protected void activate( ComponentContext context )
-    {
-        activate( context.getBundleContext() );
-    }
-
-
-    protected void deactivate( ComponentContext context )
-    {
-        deactivate();
+        return ret;
     }
 
 }
diff --git a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
index 20de138..cac6186 100644
--- a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -165,3 +165,10 @@
 config.del.config=Configuration: 
 config.del.bundle=Bundle: 
 config.unbind.ask=Are you sure to unbind this configuration ?
+
+
+# License plugin
+license.status.ok=The following bundles contains license files.
+license.status.none=No bundles with license files available
+license.resources=Bundle Resources: 
+license.resources.embedded=Embedded {0}: 
\ No newline at end of file
diff --git a/webconsole/src/main/resources/res/ui/license.css b/webconsole/src/main/resources/res/ui/license.css
index f371c35..b2bcba4 100644
--- a/webconsole/src/main/resources/res/ui/license.css
+++ b/webconsole/src/main/resources/res/ui/license.css
@@ -13,37 +13,28 @@
  * 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.
- */ /* CSS Document */
+ */
+
 #licenseContent {
-    position: relative;
-    margin-top: 25px; padding : 5px;
-    width: 100%;
-    padding: 5px;
+	width: 100%;
 }
-
 #licenseLeft {
-    width: 300px;
-    border-right: 1px black solid;
+	float: left;
+	width: 25%;
+	padding: 0;
+	margin-right: 1em;
 }
-
+#licenseLeft a {
+	display: block;
+	width: 100%;
+	margin: 0;
+	text-decoration: none;
+}
 #licenseRight {
-    position: absolute;
-    top: 5px;
-    left: 305px;
-    padding: 5px;
-    width: 650px;
-    border-left: 1px black solid;
+	float: left;
+	width: 70%;
 }
-
-#licenseClear {
-    clear: both;
-    height: 1px;
+#licenseDetails {
+	width: 100%;
+	height: 100%;
 }
-
-.licenseButtons {
-    padding: 5px;
-}
-
-.licenseDetails {
-    padding: 5px;
-}
\ No newline at end of file
diff --git a/webconsole/src/main/resources/res/ui/license.js b/webconsole/src/main/resources/res/ui/license.js
index a8cbb1f..35018d4 100644
--- a/webconsole/src/main/resources/res/ui/license.js
+++ b/webconsole/src/main/resources/res/ui/license.js
@@ -15,6 +15,9 @@
  * limitations under the License.
  */
 
+var licenseButtons = false;
+var licenseDetails = false;
+
 function displayBundle(/* String */ bundleId)
 {
     var theBundleData = bundleData[bundleId];
@@ -25,7 +28,6 @@
 
     var title = theBundleData.title;
     
-    var licenseButtons = document.getElementById('licenseButtons');
     if (licenseButtons) {
         
         var innerHTML = "";
@@ -36,47 +38,42 @@
             for (var idx in entry)
             {
                 var descr = entry[idx];
-                buttons += "<a href='javascript:displayFile(\"" + bundleId + "\", \"" + name + "\", " + idx + ");'"
-                   + " >" + descr.url + "</a> ";
+				var jar = descr.jar ? '&jar=' + descr.jar : ''; // inner jar attribute
+				buttons += '<a href="' + pluginRoot + '?bid=' + bundleId + '&url=' + descr.path + jar + '" target="licenseDetails">' + descr.url + '</a> ';
             }
             if (buttons)
             {
+				// apply i18n
+				name =  '__res__' == name ? i18n.resources : i18n.resources_emb.msgFormat( name );
                 innerHTML += name + ": " + buttons + "<br />";
             }
         }
-        
-        if (!innerHTML)
-        {
-            innerHTML = "<em>The Bundle contains neither LICENSE nor NOTICE files</em>";
-        }
-        
-        licenseButtons.innerHTML = "<h1>" + title + "</h1>" + innerHTML;
+
+        licenseButtons.html("<h1>" + title + "</h1>" + innerHTML);
     }
     
-    var licenseDetails = document.getElementById('licenseDetails');
-    if (licenseDetails)
-    {
-        licenseDetails.innerHTML = "";
-    }
+    licenseDetails.html("");
+	$("#licenseLeft a").removeClass('ui-state-default ui-corner-all');
+	$("#licenseLeft #" +bundleId).addClass('ui-state-default ui-corner-all');
 }
 
-function displayFile ( /* String */ bundleId, /* String */ name, /* int */ idx )
-{
-    var theBundleData = bundleData[bundleId];
-    if (!theBundleData)
-    {
-        return;
-    }
-    
-    var file = theBundleData.files[name][idx];
-    if (!file)
-    {
-        return;
-    }
-    
-    var licenseDetails = document.getElementById('licenseDetails');
-    if (licenseDetails)
-    {
-        licenseDetails.innerHTML = "<h3>" + name + ": " + file.url + "</h3><pre>" + file.data + "</pre>";
-    }
-}
+
+$(document).ready(function() {
+	// init elements cache
+	licenseButtons = $("#licenseButtons");
+	licenseDetails = $("#licenseDetails")
+
+	// render list of bundles
+	var txt = "";
+	for(id in bundleData) {
+		txt += '<a id="' + id + '" href="javascript:displayBundle(\'' + id + '\')">' + bundleData[id]['title'] + '</a>';
+	}
+	if (txt) {
+		$("#licenseLeft").html(txt);
+	} else {
+		$(".statline").html(i18n.status_none);
+	}
+
+	// display first element
+	for(i in bundleData) {displayBundle(i);break;}
+});
\ No newline at end of file
diff --git a/webconsole/src/main/resources/templates/license.html b/webconsole/src/main/resources/templates/license.html
new file mode 100644
index 0000000..2d59c89
--- /dev/null
+++ b/webconsole/src/main/resources/templates/license.html
@@ -0,0 +1,28 @@
+<script type="text/javascript" src="res/ui/license.js"></script>
+<script type="text/javascript">
+// <![CDATA[
+var bundleData = ${__data__};
+// i18n
+var i18n = {
+	status_ok    : '${license.status.ok}', // The following bundles contains license files.
+	status_none  : '${license.status.none}', // No bundles with license files available
+	resources    : '${license.resources}', // 'Bundle Resources'
+	resources_emb: '${license.resources.embedded}' // 'Embedded {0}'
+}
+// ]]>
+</script>
+
+<!-- status line -->
+<p class="statline">${license.status.ok}</p>
+
+<div id="licenseContent">
+	<div id="licenseLeft" class="ui-widget-content ui-corner-all">
+		<!-- here comes the bundles links -->
+	</div>
+	<div id="licenseRight">
+		<div id="licenseButtons">&nbsp;</div> 
+		<br />
+		<iframe id="licenseDetails" name="licenseDetails" frameborder="0" class="ui-widget-content"></iframe>
+	</div>
+	<div class="ui-helper-clearfix">&nbsp;</div>
+</div>