FELIX-1988 Apply 7.services.patch by Valentin Valchev (thanks) with the following change: make readTemplateFile method of AbstractWebConsole protected for use by extending plugins and fix a minor issue in the services.js script preventing mutliple service details to be opened at the same time

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@911278 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java b/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
index 20b27ea..803b09b 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
@@ -684,20 +684,42 @@
      *
      * @param templateFile The absolute path to the template file to read.
      * @return The contents of the template file as a string or and empty
-     * string if the template file fails to be read.
+     *      string if the template file fails to be read.
+     *
+     * @throws NullPointerException if <code>templateFile</code> is
+     *      <code>null</code>
+     * @throws RuntimeException if an <code>IOException</code> is thrown reading
+     *      the template file into a string. The exception provides the
+     *      exception thrown as its cause.
      */
-    private final String readTemplateFile( final String templateFile )
+    protected final String readTemplateFile( final String templateFile )
     {
-        try
+        InputStream templateStream = getClass().getResourceAsStream( templateFile );
+        if ( templateStream != null )
         {
-            return IOUtils.toString( getClass().getResourceAsStream( templateFile ), "UTF-8" );
-        }
-        catch ( IOException e )
-        {
-            log( "readTemplateFile: Error loading " + templateFile, e );
+            try
+            {
+                return IOUtils.toString( templateStream, "UTF-8" );
+            }
+            catch ( IOException e )
+            {
+                throw new RuntimeException( "readTemplateFile: Error loading " + templateFile, e );
+            }
+            finally
+            {
+                try
+                {
+                    templateStream.close();
+                }
+                catch ( IOException ignore )
+                {
+                    // ignored
+                }
+            }
         }
 
-        // fall back to empty contents to prevent NPE
+        // template file does not exist, return an empty string
+        log( "readTemplateFile: File '" + templateFile + "' not found through class " + getClass() );
         return "";
     }
 
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
index 07179ea..14629bc 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
@@ -30,9 +30,11 @@
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.felix.webconsole.ConfigurationPrinter;
+import org.apache.felix.webconsole.DefaultVariableResolver;
+import org.apache.felix.webconsole.SimpleWebConsolePlugin;
 import org.apache.felix.webconsole.WebConsoleConstants;
 import org.apache.felix.webconsole.WebConsoleUtil;
-import org.apache.felix.webconsole.internal.BaseWebConsolePlugin;
+import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -44,18 +46,21 @@
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
-import org.osgi.service.log.LogService;
 
 
-public class ServicesServlet extends BaseWebConsolePlugin implements ConfigurationPrinter
+/**
+ * ServicesServlet provides a plugin for inspecting the registered services.
+ */
+public class ServicesServlet extends SimpleWebConsolePlugin implements ConfigurationPrinter, OsgiManagerPlugin
 {
+    // don't create empty reference array all the time, create it only once - it is immutable
+    private static final ServiceReference[] NO_REFS = new ServiceReference[0];
 
     private final class RequestInfo
     {
         public final String extension;
         public final ServiceReference service;
         public final boolean serviceRequested;
-        public final String pathInfo;
 
 
         protected RequestInfo( final HttpServletRequest request )
@@ -86,13 +91,11 @@
             {
                 service = null;
                 serviceRequested = false;
-                pathInfo = null;
             }
             else
             {
                 service = getServiceById( serviceInfo );
                 serviceRequested = true;
-                pathInfo = serviceInfo;
             }
             request.setAttribute( ServicesServlet.class.getName(), this );
         }
@@ -100,18 +103,32 @@
     }
 
 
-    public static RequestInfo getRequestInfo( final HttpServletRequest request )
+    static RequestInfo getRequestInfo( final HttpServletRequest request )
     {
         return ( RequestInfo ) request.getAttribute( ServicesServlet.class.getName() );
     }
 
     private ServiceRegistration configurationPrinter;
 
+    /** the label for the services plugin */
     public static final String LABEL = "services";
+    private static final String TITLE = "Services";
+    private static final String CSS[] = null;
 
-    public static final String TITLE = "Services";
+    private final String TEMPLATE;
+
+    /** Default constructor */
+    public ServicesServlet() {
+        super(LABEL, TITLE, CSS);
+
+        // load templates
+        TEMPLATE = readTemplateFile( "/templates/services.html" );
+    }
 
 
+    /**
+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#activate(org.osgi.framework.BundleContext)
+     */
     public void activate( BundleContext bundleContext )
     {
         super.activate( bundleContext );
@@ -119,6 +136,9 @@
     }
 
 
+    /**
+     * @see org.apache.felix.webconsole.SimpleWebConsolePlugin#deactivate()
+     */
     public void deactivate()
     {
         if ( configurationPrinter != null )
@@ -131,18 +151,9 @@
     }
 
 
-    public String getLabel()
-    {
-        return LABEL;
-    }
-
-
-    public String getTitle()
-    {
-        return TITLE;
-    }
-
-
+    /**
+     * @see org.apache.felix.webconsole.ConfigurationPrinter#printConfiguration(java.io.PrintWriter)
+     */
     public void printConfiguration( PrintWriter pw )
     {
         try
@@ -215,12 +226,12 @@
         }
         catch ( Exception e )
         {
-            getLog().log( LogService.LOG_ERROR, "Problem rendering Bundle details for configuration status", e );
+            log( "Problem rendering Bundle details for configuration status", e );
         }
     }
 
 
-    private void appendServiceInfoCount( final StringBuffer buf, String msg, int count )
+    private static final void appendServiceInfoCount( final StringBuffer buf, String msg, int count )
     {
         buf.append( count );
         buf.append( " service" );
@@ -231,7 +242,7 @@
     }
 
 
-    private ServiceReference getServiceById( String pathInfo )
+    final ServiceReference getServiceById( String pathInfo )
     {
         // only use last part of the pathInfo
         pathInfo = pathInfo.substring( pathInfo.lastIndexOf( '/' ) + 1 );
@@ -251,14 +262,14 @@
         }
         catch ( InvalidSyntaxException e )
         {
-            getLog().log( LogService.LOG_WARNING, "Unable to search for services using filter " + filterStr, e );
+            log( "Unable to search for services using filter " + filterStr, e );
             // this shouldn't happen
             return null;
         }
     }
 
 
-    private ServiceReference[] getServices()
+    private final ServiceReference[] getServices()
     {
         try
         {
@@ -266,13 +277,13 @@
         }
         catch ( InvalidSyntaxException e )
         {
-            getLog().log( LogService.LOG_WARNING, "Unable to access service reference list.", e );
-            return new ServiceReference[0];
+            log( "Unable to access service reference list.", e );
+            return NO_REFS;
         }
     }
 
 
-    private String getStatusLine( final ServiceReference[] services )
+    private static final String getStatusLine( final ServiceReference[] services )
     {
         final StringBuffer buffer = new StringBuffer();
         buffer.append( "Services information: " );
@@ -418,7 +429,8 @@
         throws IOException
     {
         final ServiceReference[] allServices = this.getServices();
-        final String statusLine = this.getStatusLine( allServices );
+        final String statusLine = getStatusLine( allServices );
+
         final ServiceReference[] services = ( service != null ) ? new ServiceReference[]
             { service } : allServices;
 
@@ -431,6 +443,9 @@
             jw.key( "status" );
             jw.value( statusLine );
 
+            jw.key( "serviceCount" );
+            jw.value( allServices.length );
+
             jw.key( "data" );
 
             jw.array();
@@ -453,50 +468,53 @@
     }
 
 
+    /**
+     * @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
     {
-        final RequestInfo reqInfo = new RequestInfo( request );
-        if ( reqInfo.service == null && reqInfo.serviceRequested )
-        {
-            response.sendError( 404 );
-            return;
-        }
-        if ( reqInfo.extension.equals( "json" ) )
-        {
-            this.renderJSON( response, reqInfo.service );
+        if (request.getPathInfo().indexOf("/res/") == -1)
+        { // not resource
+            final RequestInfo reqInfo = new RequestInfo( request );
+            if ( reqInfo.service == null && reqInfo.serviceRequested )
+            {
+                response.sendError( 404 );
+                return;
+            }
+            if ( reqInfo.extension.equals( "json" ) )
+            {
+                this.renderJSON( response, reqInfo.service );
 
-            // nothing more to do
-            return;
+                // nothing more to do
+                return;
+            }
         }
 
         super.doGet( request, response );
     }
 
 
-    protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws ServletException,
-        IOException
+    /**
+     * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException
     {
         // get request info from request attribute
         final RequestInfo reqInfo = getRequestInfo( request );
-        final PrintWriter pw = response.getWriter();
 
         final String appRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT );
+        StringWriter w = new StringWriter();
+        PrintWriter w2 = new PrintWriter(w);
+        writeJSON(w2, reqInfo.service);
 
-        Util.startScript( pw );
-        pw.println( "var imgRoot = '" + appRoot + "/res/imgs';" );
-        pw.println( "var bundlePath = '" + appRoot + "/" + BundlesServlet.NAME + "/" + "';" );
-        pw.println( "var drawDetails = " + reqInfo.serviceRequested + ";" );
-        Util.endScript( pw );
+        // prepare variables
+        DefaultVariableResolver vars = ( ( DefaultVariableResolver ) WebConsoleUtil.getVariableResolver( request ) );
+        vars.put( "bundlePath", appRoot +  "/" + BundlesServlet.NAME + "/" );
+        vars.put( "drawDetails", reqInfo.serviceRequested ? Boolean.TRUE : Boolean.FALSE );
+        vars.put( "__data__", w.toString() );
 
-        Util.script( pw, appRoot, "services.js" );
-
-        pw.println( "<div id='plugin_content'/>" );
-        Util.startScript( pw );
-        pw.print( "renderServices(" );
-        writeJSON( pw, reqInfo.service );
-        pw.println( ");" );
-        Util.endScript( pw );
-
+        response.getWriter().print( TEMPLATE );
     }
+
 }
diff --git a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
index 273811f..6340ef0 100644
--- a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -28,6 +28,12 @@
 reload=Reload
 change=Change
 abort=Abort
+back=Back
+id=Id
+type=Type
+type_s=Type(s)
+bundle=Bundle
+
 
 # VMStat plugin
 vmstat.stopped=Framework has been stopped.
@@ -50,4 +56,8 @@
 vmstat.gc.title=Garbage Collection
 vmstat.gc.button=Run
 
-
+# Services plugin
+services.details.hide=Hide Details
+services.details.tip=Details
+services.statusline=Services information: {0} service(s) in total.
+services.caption=Services
\ No newline at end of file
diff --git a/webconsole/src/main/resources/res/lib/support.js b/webconsole/src/main/resources/res/lib/support.js
index 2310b3e..86e7620 100644
--- a/webconsole/src/main/resources/res/lib/support.js
+++ b/webconsole/src/main/resources/res/lib/support.js
@@ -60,7 +60,7 @@
 /* automatically executed on load */
 $(document).ready(function() {
 	// init table-sorter tables - only once!
-	var tables = $('table.tablesorter');
+	var tables = $('table.tablesorter:not(.noauto)');
 	if (tables.size() > 0) tables.tablesorter();
 	
 	// init navigation
@@ -80,9 +80,33 @@
 	initStaticWidgets();
 });
 
+/* A helper function, used together with tablesorter, when the cells contains mixed text and links. As example:
+
+	elem.tablesorter({
+		headers: {
+			0: {textExtraction: mixedLinksExtraction},
+			2: {sorter: false}
+		}
+	});
+*/
+function mixedLinksExtraction(node) {
+	var l = node.getElementsByTagName('a');
+	return l && l.length > 0 ? l[0].innerHTML : node.innerHTML;
+};
+
+/* Java-like MessageFormat method. Usage:
+	'hello {0}'.msgFormat('world')
+*/
+String.prototype.msgFormat = function(/* variable arguments*/) {
+	var i=0; var s=this;
+	while(i<arguments.length) s=s.replace('{'+i+'}',arguments[i++]);
+	return s;
+}
+
+
 /* replacement for confirm() method, needs 'action' parameter to work.
  * if action is not set - then default confirm() method is used. */
-function Xconfirm(text, action, title) {
+function Xconfirm(/* String */text, /* Callback function */action, /* String */title) {
 	if (!$.isFunction(action)) return confirm(text);
 	if (title === undefined) title = "";
 
@@ -101,7 +125,7 @@
 	});
 	return false;
 }
-function Xalert(text, title) {
+function Xalert(/* String */text, /* String */title) {
 	if (!$.isFunction(action)) return alert(text);
 	if (title === undefined) title = "";
 
diff --git a/webconsole/src/main/resources/res/ui/services.js b/webconsole/src/main/resources/res/ui/services.js
index 29691e5..db3058c 100644
--- a/webconsole/src/main/resources/res/ui/services.js
+++ b/webconsole/src/main/resources/res/ui/services.js
@@ -14,30 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-function renderStatusLine() {
-	$("#plugin_content").append(
-			"<div class='fullwidth'><div class='statusline'/></div>");
-}
-
-function renderView( /* Array of String */columns) {
-	renderStatusLine();
-	var txt = "<div class='table'><table id='plugin_table' class='tablelayout'><thead><tr>";
-	for ( var name in columns) {
-		txt = txt + "<th class='col_" + columns[name] + "'>" + columns[name]
-				+ "</th>";
-	}
-	txt = txt + "</tr></thead><tbody></tbody></table></div>";
-	$("#plugin_content").append(txt);
-	renderStatusLine();
-}
 
 function renderData(eventData) {
-	$(".statusline").empty().append(eventData.status);
-	$("#plugin_table > tbody > tr").remove();
+	$('.statline').empty().append(i18n.statline.msgFormat(eventData.serviceCount));
+	$('#plugin_table > tbody > tr').remove();
 	for ( var idx in eventData.data) {
 		entry(eventData.data[idx]);
 	}
-	$("#plugin_table").trigger("update");
+	$('#plugin_table').trigger('update');
 	if (drawDetails) {
 		renderDetails(eventData);
 	}
@@ -45,27 +29,23 @@
 
 function entry( /* Object */dataEntry) {
 	var trElement = tr(null, {
-		id : "entry" + dataEntry.id
+		id : 'entry' + dataEntry.id
 	});
 	entryInternal(trElement, dataEntry);
-	$("#plugin_table > tbody").append(trElement);
+	$('#plugin_table > tbody').append(trElement);
 }
 
 function entryInternal( /* Element */parent, /* Object */dataEntry) {
 	var id = dataEntry.id;
 	var name = dataEntry.id;
 
-	var inputElement = createElement("img", "rightButton", {
-		src : appRoot + "/res/imgs/arrow_right.png",
-		style : {
-			border : "none"
-		},
-		id : 'img' + id,
-		title : "Details",
-		alt : "Details",
-		width : 14,
-		height : 14
+	// right arrow
+	var inputElement = createElement('span', 'ui-icon ui-icon-triangle-1-e', {
+		title: i18n.detailsTip,
+		id: 'img' + id,
+		style: {display: 'inline-block'}
 	});
+
 	$(inputElement).click(function() {
 		showDetails(id)
 	});
@@ -73,127 +53,112 @@
 	if (drawDetails) {
 		titleElement = text(name);
 	} else {
-		titleElement = createElement("a", null, {
-			href : window.location.pathname + "/" + id
+		titleElement = createElement('a', null, {
+			href : window.location.pathname + '/' + id
 		});
 		titleElement.appendChild(text(name));
 	}
-	var bundleElement = createElement("a", null, {
+	var bundleElement = createElement('a', null, {
 		href : bundlePath + dataEntry.bundleId
 	});
-	bundleElement.appendChild(text(dataEntry.bundleSymbolicName + " ("
-			+ dataEntry.bundleId + ")"));
+	bundleElement.appendChild(text(dataEntry.bundleSymbolicName + ' ('
+			+ dataEntry.bundleId + ')'));
 
-	parent
-			.appendChild(td(null, null,
-					[ inputElement, text(" "), titleElement ]));
+	parent.appendChild(td(null, null, [ inputElement, text(' '), titleElement ]));
 	parent.appendChild(td(null, null, [ text(dataEntry.types) ]));
 	parent.appendChild(td(null, null, [ bundleElement ]));
 }
 
 function showDetails(id) {
-	$.get(pluginRoot + "/" + id + ".json", null, function(data) {
+	$.get(pluginRoot + '/' + id + '.json', null, function(data) {
 		renderDetails(data);
-	}, "json");
+	}, 'json');
 }
 
 function hideDetails(id) {
-	$("#img" + id).each(function() {
-		$("#pluginInlineDetails").remove();
-		$(this).attr("src", appRoot + "/res/imgs/arrow_right.png");
-		$(this).attr("title", "Details");
-		$(this).attr("alt", "Details");
-		$(this).unbind('click').click(function() {
-			showDetails(id)
-		});
+	$('#img' + id).each(function() {
+		$('#pluginInlineDetails' + id).remove();
+		$(this).
+			removeClass('ui-icon-triangle-1-w').//left
+			removeClass('ui-icon-triangle-1-s').//down
+			addClass('ui-icon-triangle-1-e').//right
+			unbind('click').click(function() {showDetails(id)});
 	});
 }
 
 function renderDetails(data) {
 	data = data.data[0];
-	$("#pluginInlineDetails").remove();
-	$("#entry" + data.id + " > td").eq(1).append(
-			"<div id='pluginInlineDetails'/>");
-	$("#img" + data.id).each(function() {
+	$('#entry' + data.id + ' > td').eq(1).append('<div id="pluginInlineDetails' + data.id + '"/>');
+	$('#img' + data.id).each(function() {
 		if (drawDetails) {
-			$(this).attr("src", appRoot + "/res/imgs/arrow_left.png");
-			$(this).attr("title", "Back");
-			$(this).attr("alt", "Back");
 			var ref = window.location.pathname;
 			ref = ref.substring(0, ref.lastIndexOf('/'));
-			$(this).unbind('click').click(function() {
-				window.location = ref;
-			});
+			$(this).
+				removeClass('ui-icon-triangle-1-e').//right
+				removeClass('ui-icon-triangle-1-s').//down
+				addClass('ui-icon-triangle-1-w').//left
+				attr('title', i18n.back).
+				unbind('click').click(function() {window.location = ref;});
 		} else {
-			$(this).attr("src", appRoot + "/res/imgs/arrow_down.png");
-			$(this).attr("title", "Hide Details");
-			$(this).attr("alt", "Hide Details");
-			$(this).unbind('click').click(function() {
-				hideDetails(data.id)
-			});
+			$(this).
+				removeClass('ui-icon-triangle-1-w').//left
+				removeClass('ui-icon-triangle-1-e').//right
+				addClass('ui-icon-triangle-1-s').//down
+				attr('title', i18n.detailsHide).
+				unbind('click').click(function() {hideDetails(data.id)});
 		}
 	});
-	$("#pluginInlineDetails").append(
-			"<table border='0'><tbody></tbody></table>");
-	var details = data.props;
-	for ( var idx in details) {
+	if (data.props) 
+		$('#pluginInlineDetails' + data.id).append( renderObjectAsTable(data.props) );
+}
+
+function renderObjectAsTable(/* Object*/ details) {
+	var txt = '';
+
+	for (var idx in details) {
 		var prop = details[idx];
 
-		var txt = "<tr><td class='aligntop' noWrap='true' style='border:0px none'>"
+		txt += '<tr><td class="aligntop" noWrap="true" style="border:0px none">'
 				+ prop.key
-				+ "</td><td class='aligntop' style='border:0px none'>";
+				+ '</td><td class="aligntop" style="border:0px none">';
 		if (prop.value) {
 			if ($.isArray(prop.value)) {
 				var i = 0;
 				for ( var pi in prop.value) {
 					var value = prop.value[pi];
 					if (i > 0) {
-						txt = txt + "<br/>";
+						txt = txt + '<br/>';
 					}
-					var span;
-					if (value.substring(0, 6) == "INFO: ") {
-						txt = txt + "<span style='color: grey;'>!!"
-								+ value.substring(5) + "</span>";
-					} else if (value.substring(0, 7) == "ERROR: ") {
-						txt = txt + "<span style='color: red;'>!!"
-								+ value.substring(6) + "</span>";
-					} else {
-						txt = txt + value;
-					}
+					txt = txt + value;
 					i++;
 				}
 			} else {
 				txt = txt + prop.value;
 			}
 		} else {
-			txt = txt + "\u00a0";
+			txt = txt + '\u00a0';
 		}
-		txt = txt + "</td></tr>";
-		$("#pluginInlineDetails > table > tbody").append(txt);
-
+		txt = txt + '</td></tr>';
 	}
+
+	if ( txt ) {
+		txt = '<table border="0"><tbody>' + txt + '</tbody></table>';
+	}
+
+	return txt;
 }
 
 function renderServices(data) {
 	$(document).ready(function() {
-		renderView( [ "Id", "Type(s)", "Bundle" ]);
 		renderData(data);
 
-		var extractMethod = function(node) {
-			var link = node.getElementsByTagName("a");
-			if (link && link.length == 1) {
-				return link[0].innerHTML;
-			}
-			return node.innerHTML;
-		};
-		$("#plugin_table").tablesorter( {
+		$('#plugin_table').tablesorter( {
 			headers : {
-				0 : {
-					sorter : "digit"
-				}
+				0 : { sorter : 'digit' }
 			},
 			sortList : [ [ 1, 0 ] ],
-			textExtraction : extractMethod
+			textExtraction : mixedLinksExtraction,
+			widgets: ['zebra']
 		});
 	});
 }
\ No newline at end of file
diff --git a/webconsole/src/main/resources/templates/services.html b/webconsole/src/main/resources/templates/services.html
new file mode 100644
index 0000000..0d5201f
--- /dev/null
+++ b/webconsole/src/main/resources/templates/services.html
@@ -0,0 +1,38 @@
+<script type="text/javascript" src="res/ui/services.js"></script>
+<script type="text/javascript">
+// <![CDATA[
+// i18n stuff
+var i18n = {
+	back : "${back}",
+	detailsHide : "${services.details.hide}",
+	detailsTip : "${services.details.tip}",
+	statline : "${services.statusline}"
+}
+
+// data
+var bundlePath = "${bundlePath}";
+var drawDetails = ${drawDetails};
+renderServices(${__data__});
+// ]]>
+</script>
+
+<p class="statline">&nbsp;</p>
+
+<div class="ui-widget-header ui-corner-top buttonGroup">${services.caption}</div>
+
+<table id="plugin_table" class="tablesorter nicetable noauto">
+	<thead>
+		<tr>
+			<th class="col_Id">${id}</th>
+			<th class="col_Types">${type_s}</th>
+			<th class="col_Bundle">${bundle}</th>
+		</tr>
+	</thead>
+	<tbody>
+		<tr><td colspan="3">dummy</td></tr>
+	</tbody>
+</table>
+
+<div class="ui-widget-header ui-corner-bottom buttonGroup">&nbsp;</div>
+	
+<p class="statline">&nbsp;</p>