FELIX-1051 Add Localization Support based on Bundle-Localization header. Also included is a first shot at trying to implement some templating support. See issue for details.
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@902097 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 12281af..077924a 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
@@ -32,7 +32,7 @@
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
-import org.apache.felix.webconsole.internal.WebConsolePluginAdapter;
+import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
@@ -186,6 +186,12 @@
}
+ /**
+ * Returns the <code>BundleContext</code> with which this plugin has been
+ * activated. If the plugin has not be activated by calling the
+ * {@link #activate(BundleContext)} method, this method returns
+ * <code>null</code>.
+ */
protected BundleContext getBundleContext()
{
return bundleContext;
@@ -193,6 +199,20 @@
/**
+ * Returns the <code>Bundle</code> pertaining to the
+ * {@link #getBundleContext() bundle context} with which this plugin has
+ * been activated. If the plugin has not be activated by calling the
+ * {@link #activate(BundleContext)} method, this method returns
+ * <code>null</code>.
+ */
+ public final Bundle getBundle()
+ {
+ final BundleContext bundleContext = getBundleContext();
+ return ( bundleContext != null ) ? bundleContext.getBundle() : null;
+ }
+
+
+ /**
* Returns the object which might provide resources. The class of this
* object is used to find the <code>getResource</code> method.
* <p>
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/VariableResolver.java b/webconsole/src/main/java/org/apache/felix/webconsole/VariableResolver.java
new file mode 100644
index 0000000..cc50236
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/VariableResolver.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+
+/**
+ * The <code>VariableResolver</code> interface is a very simple interface which
+ * may be implemented by Web Console plugins to provide replacement values for
+ * variables in the generated content.
+ * <p>
+ * The main use of such a variable resolve is when a plugin is using a static
+ * template which provides slots to place dynamically generated content
+ * parts.
+ */
+public interface VariableResolver
+{
+
+ /**
+ * Default implementation of the {@link VariableResolver} interface whose
+ * {@link #get(String)} method always returns <code>null</code>.
+ */
+ VariableResolver DEFAULT = new VariableResolver()
+ {
+ public String get( String variable )
+ {
+ return null;
+ }
+ };
+
+
+ /**
+ * Return a replacement value for the named variable or <code>null</code>
+ * if no replacement is available.
+ *
+ * @param variable The name of the variable for which to return a
+ * replacement.
+ * @return The replacement value or <code>null</code> if no replacement is
+ * available.
+ */
+ String get( String variable );
+
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
index 2a79580..4d0b10c 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
@@ -20,6 +20,7 @@
import java.io.IOException;
+import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
@@ -29,6 +30,7 @@
import javax.servlet.http.HttpServletResponse;
import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.VariableResolver;
import org.apache.felix.webconsole.WebConsoleConstants;
import org.osgi.framework.ServiceReference;
@@ -39,7 +41,7 @@
* {@link org.apache.felix.webconsole.WebConsoleConstants#PLUGIN_TITLE}
* service attribute.
*/
-public class WebConsolePluginAdapter extends AbstractWebConsolePlugin
+public class WebConsolePluginAdapter extends AbstractWebConsolePlugin implements VariableResolver
{
/** serial UID */
@@ -57,6 +59,9 @@
// the CSS references (null if none)
private final String[] cssReferences;
+ // the delegatee variable resolver
+ private VariableResolver varResolver;
+
public WebConsolePluginAdapter( String label, String title, Servlet plugin, ServiceReference serviceReference )
{
@@ -64,6 +69,9 @@
this.title = title;
this.plugin = plugin;
this.cssReferences = toStringArray( serviceReference.getProperty( WebConsoleConstants.PLUGIN_CSS_REFERENCES ) );
+
+ // activate this abstract plugin (mainly to set the bundle context)
+ activate( serviceReference.getBundle().getBundleContext() );
}
@@ -127,13 +135,28 @@
*/
public void init( ServletConfig config ) throws ServletException
{
- // base classe initialization
- super.init( config );
+ // no need to activate the plugin, this has already been done
+ // when the instance was setup
+ try
+ {
+ // base classe initialization
+ super.init( config );
- // plugin initialization
- plugin.init( config );
+ // plugin initialization
+ plugin.init( config );
+ }
+ catch ( ServletException se )
+ {
+ // if init fails, the plugin will not be destroyed and thus
+ // the plugin not deactivated. Do it here
+ deactivate();
+
+ // rethrow the exception
+ throw se;
+ }
}
+
/**
* Detects whether this request is intended to have the headers and
* footers of this plugin be rendered or not. The decision is taken based
@@ -178,11 +201,26 @@
*/
public void destroy()
{
- plugin.destroy();
- super.destroy();
+ try
+ {
+ plugin.destroy();
+ super.destroy();
+ }
+ finally
+ {
+ varResolver = null;
+ deactivate();
+ }
}
+ //---------- VariableResolver
+
+ public String get( String variable )
+ {
+ return getVariableResolver().get(variable);
+ }
+
//---------- internal
private String[] toStringArray( final Object value )
@@ -222,4 +260,69 @@
return null;
}
+
+
+ private VariableResolver getVariableResolver()
+ {
+ if ( varResolver == null )
+ {
+ if ( plugin instanceof VariableResolver )
+ {
+ varResolver = ( VariableResolver ) plugin;
+ }
+ else
+ {
+ varResolver = VariableResolverProxy.create( plugin );
+ }
+ }
+
+ return varResolver;
+ }
+
+ private static class VariableResolverProxy implements VariableResolver
+ {
+ static VariableResolver create( Object object )
+ {
+ try
+ {
+ final Class stringClass = String.class;
+ final Method getMethod = object.getClass().getMethod( "get", new Class[]
+ { stringClass } );
+ if ( getMethod.getReturnType() == stringClass )
+ {
+ return new VariableResolverProxy( object, getMethod );
+ }
+ }
+ catch ( Throwable t )
+ {
+ }
+
+ return VariableResolver.DEFAULT;
+ }
+
+ private final Object object;
+
+ private final Method getMethod;
+
+
+ private VariableResolverProxy( final Object object, final Method getMethod )
+ {
+ this.object = object;
+ this.getMethod = getMethod;
+ }
+
+
+ public String get( String variable )
+ {
+ try
+ {
+ return ( String ) getMethod.invoke( object, new Object[]
+ { variable } );
+ }
+ catch ( Throwable t )
+ {
+ return null;
+ }
+ }
+ }
}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
new file mode 100644
index 0000000..3b2885e
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
@@ -0,0 +1,97 @@
+/*
+ * 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.filter;
+
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ResourceBundle;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import org.apache.felix.webconsole.VariableResolver;
+
+
+/**
+ * The <code>FilteringResponseWrapper</code> wraps the response to provide a
+ * filtering writer for HTML responses. The filtering writer filters the
+ * response such that any string of the form <code>${some text}</code> is
+ * replaced by a translation of the <i>some text</i> according to the
+ * <code>ResourceBundle</code> provided in the constructor. If no translation
+ * exists in the resource bundle, the text is written unmodifed (except the
+ * wrapping <code>${}</code> characters are removed.
+ */
+public class FilteringResponseWrapper extends HttpServletResponseWrapper
+{
+
+ // the resource bundle providing translations for the output
+ private final ResourceBundle locale;
+
+ private final VariableResolver variables;
+
+ // the writer sending output in this response
+ private PrintWriter writer;
+
+
+ /**
+ * Creates a wrapper instance using the given resource bundle for
+ * translations.
+ */
+ public FilteringResponseWrapper( HttpServletResponse response, ResourceBundle locale, VariableResolver variables )
+ {
+ super( response );
+ this.locale = locale;
+ this.variables = variables;
+ }
+
+
+ /**
+ * Returns a <code>PrintWriter</code> for the response. If <code>text/html</code>
+ * is being generated a filtering writer is returned which translates
+ * strings enclosed in <code>${}</code> according to the resource bundle
+ * configured for this response.
+ */
+ public PrintWriter getWriter() throws IOException
+ {
+ if ( writer == null )
+ {
+ final PrintWriter base = super.getWriter();
+ if ( doWrap() )
+ {
+ final ResourceFilteringWriter filter = new ResourceFilteringWriter( base, locale, variables );
+ writer = new PrintWriter( filter );
+ }
+ else
+ {
+ writer = base;
+ }
+ }
+
+ return writer;
+ }
+
+
+ private boolean doWrap()
+ {
+ boolean doWrap = getContentType() != null && getContentType().indexOf( "text/html" ) >= 0;
+ return doWrap;
+ }
+
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/ResourceFilteringWriter.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/ResourceFilteringWriter.java
new file mode 100644
index 0000000..ed38cdf
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/ResourceFilteringWriter.java
@@ -0,0 +1,232 @@
+/*
+ * 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.filter;
+
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import org.apache.felix.webconsole.VariableResolver;
+
+
+/**
+ * The <code>ResourceFilteringWriter</code> is a writer, which translates
+ * strings of the form <code>${some key text}</code> to a translation
+ * of the respective <i>some key text</i> or to the <i>some key text</i>
+ * itself if no translation is available from a resource bundle.
+ */
+class ResourceFilteringWriter extends FilterWriter
+{
+
+ /**
+ * normal processing state, $ signs are recognized here
+ * proceeds to {@link #STATE_DOLLAR} if a $ sign is encountered
+ * proceeds to {@link #STATE_ESCAPE} if a \ sign is encountered
+ * otherwise just writes the character
+ */
+ private static final int STATE_NULL = 0;
+
+ /**
+ * State after a $ sign has been recognized
+ * proceeds to {@value #STATE_BUFFERING} if a { sign is encountered
+ * otherwise proceeds to {@link #STATE_NULL} and writes the $ sign and
+ * the current character
+ */
+ private static final int STATE_DOLLAR = 1;
+
+ /**
+ * buffers characters until a } is encountered
+ * proceeds to {@link #STATE_NULL} if a } sign is encountered and
+ * translates and writes buffered text before returning
+ * otherwise collects characters to gather the translation key
+ */
+ private static final int STATE_BUFFERING = 2;
+
+ /**
+ * escaping the next character, if the character is a $ sign, the
+ * $ sign is writeted. otherwise the \ and the next character is
+ * written
+ * proceeds to {@link #STATE_NULL}
+ */
+ private static final int STATE_ESCAPE = 3;
+
+ /**
+ * The ResourceBundle used for translation
+ */
+ private final ResourceBundle locale;
+
+ private final VariableResolver variables;
+
+ /**
+ * The buffer to gather the text to be translated
+ */
+ private final StringBuffer lineBuffer = new StringBuffer();
+
+ /**
+ * The current state, starts with {@link #STATE_NULL}
+ */
+ private int state = STATE_NULL;
+
+
+ ResourceFilteringWriter( Writer out, ResourceBundle locale, final VariableResolver variables )
+ {
+ super( out );
+ this.locale = locale;
+ this.variables = ( variables != null ) ? variables : VariableResolver.DEFAULT;
+ }
+
+
+ /**
+ * Write a single character following the state machine:
+ * <table>
+ * <tr><th>State</th><th>Character</th><th>Task</th><th>Next State</th></tr>
+ * <tr><td>NULL</td><td>$</td><td> </td><td>DOLLAR</td></tr>
+ * <tr><td>NULL</td><td>\</td><td> </td><td>ESCAPE</td></tr>
+ * <tr><td>NULL</td><td>any</td><td>write c</td><td>NULL</td></tr>
+ * <tr><td>DOLLAR</td><td>{</td><td> </td><td>BUFFERING</td></tr>
+ * <tr><td>DOLLAR</td><td>any</td><td>write $ and c</td><td>NULL</td></tr>
+ * <tr><td>BUFFERING</td><td>}</td><td>translate and write translation</td><td>NULL</td></tr>
+ * <tr><td>BUFFERING</td><td>any</td><td>buffer c</td><td>BUFFERING</td></tr>
+ * <tr><td>ESACPE</td><td>$</td><td>write $</td><td>NULL</td></tr>
+ * <tr><td>ESCAPE</td><td>any</td><td>write \ and c</td><td>NULL</td></tr>
+ * </table>
+ *
+ * @exception IOException If an I/O error occurs
+ */
+ public void write( int c ) throws IOException
+ {
+ switch ( state )
+ {
+ case STATE_NULL:
+ if ( c == '$' )
+ {
+ state = STATE_DOLLAR;
+ }
+ else if ( c == '\\' )
+ {
+ state = STATE_ESCAPE;
+ }
+ else
+ {
+ out.write( c );
+ }
+ break;
+
+ case STATE_DOLLAR:
+ if ( c == '{' )
+ {
+ state = STATE_BUFFERING;
+ }
+ else
+ {
+ out.write( '$' );
+ out.write( c );
+ }
+ break;
+
+ case STATE_BUFFERING:
+ if ( c == '}' )
+ {
+ state = STATE_NULL;
+ super.write( translate() );
+ }
+ else
+ {
+ lineBuffer.append( ( char ) c );
+ }
+ break;
+
+ case STATE_ESCAPE:
+ state = STATE_NULL;
+ if ( c != '$' )
+ {
+ out.write( '\\' );
+ }
+ out.write( c );
+ }
+ }
+
+
+ /**
+ * Writes each character calling {@link #write(int)}
+ *
+ * @param cbuf Buffer of characters to be written
+ * @param off Offset from which to start reading characters
+ * @param len Number of characters to be written
+ * @exception IOException If an I/O error occurs
+ */
+ public void write( char cbuf[], int off, int len ) throws IOException
+ {
+ final int limit = off + len;
+ for ( int i = off; i < limit; i++ )
+ {
+ write( cbuf[i] );
+ }
+ }
+
+
+ /**
+ * Writes each character calling {@link #write(int)}
+ *
+ * @param str String to be written
+ * @param off Offset from which to start reading characters
+ * @param len Number of characters to be written
+ * @exception IOException If an I/O error occurs
+ */
+ public void write( String str, int off, int len ) throws IOException
+ {
+ final int limit = off + len;
+ for ( int i = off; i < limit; i++ )
+ {
+ write( str.charAt( i ) );
+ }
+ }
+
+
+ /**
+ * Translates the current buffer contents and returns the translation.
+ * First the name is looked up in the variables, if not found the
+ * resource bundle is queried. If still not found, the buffer contents
+ * is returned unmodified.
+ */
+ private String translate()
+ {
+ final String key = lineBuffer.toString();
+ lineBuffer.delete( 0, lineBuffer.length() );
+
+ String value = variables.get( key );
+ if ( value == null )
+ {
+ try
+ {
+ value = locale.getString( key );
+ }
+ catch ( MissingResourceException mre )
+ {
+ // ignore and write the key as the value
+ value = key;
+ }
+ }
+
+ return value;
+ }
+}
\ No newline at end of file
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedEnumeration.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedEnumeration.java
new file mode 100644
index 0000000..5d0441b
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedEnumeration.java
@@ -0,0 +1,107 @@
+/*
+ * 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.i18n;
+
+
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+
+/**
+ * The <code>CombinedEnumeration</code> combines two enumerations into a single
+ * one first returning everything from the first enumeration and then from the
+ * second enumeration with a single limitation: entries are only returned once.
+ * So if both enumerations would produce the same result, say "123", only the
+ * first would be returned.
+ */
+class CombinedEnumeration implements Enumeration
+{
+
+ // the first enumeration to iterate
+ private final Enumeration first;
+
+ // the second enumeration to iterate once the first is exhausted
+ private final Enumeration second;
+
+ // the set of values already returned to prevent duplicate entries
+ private final Set seenKeys;
+
+ // preview to the next return value for nextElement(), null at the end
+ private Object nextKey;
+
+
+ CombinedEnumeration( final Enumeration first, final Enumeration second )
+ {
+ this.first = first;
+ this.second = second;
+
+ this.seenKeys = new HashSet();
+ this.nextKey = seek();
+ }
+
+
+ public boolean hasMoreElements()
+ {
+ return nextKey != null;
+ }
+
+
+ public Object nextElement()
+ {
+ if ( !hasMoreElements() )
+ {
+ throw new NoSuchElementException();
+ }
+
+ Object result = nextKey;
+ nextKey = seek();
+ return result;
+ }
+
+
+ /**
+ * Check the enumerations for the next element to return. If no more
+ * (unique) element is available, null is returned. The element returned
+ * is also added to the set of seen elements to prevent duplicate provision
+ */
+ private Object seek()
+ {
+ while ( first.hasMoreElements() )
+ {
+ final Object next = first.nextElement();
+ if ( !seenKeys.contains( next ) )
+ {
+ seenKeys.add( next );
+ return next;
+ }
+ }
+ while ( second.hasMoreElements() )
+ {
+ final Object next = second.nextElement();
+ if ( !seenKeys.contains( next ) )
+ {
+ seenKeys.add( next );
+ return next;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedResourceBundle.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedResourceBundle.java
new file mode 100644
index 0000000..6b6c44c
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/CombinedResourceBundle.java
@@ -0,0 +1,73 @@
+/*
+ * 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.i18n;
+
+
+import java.util.Enumeration;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+
+class CombinedResourceBundle extends ResourceBundle
+{
+
+ private final ResourceBundle resourceBundle;
+ private final ResourceBundle defaultResourceBundle;
+
+
+ CombinedResourceBundle( final ResourceBundle resourceBundle, final ResourceBundle defaultResourceBundle )
+ {
+ this.resourceBundle = resourceBundle;
+ this.defaultResourceBundle = defaultResourceBundle;
+ }
+
+
+ public Enumeration getKeys()
+ {
+ return new CombinedEnumeration( resourceBundle.getKeys(), defaultResourceBundle.getKeys() );
+ }
+
+
+ protected Object handleGetObject( String key )
+ {
+ // check primary resource bundle first
+ try
+ {
+ return resourceBundle.getObject( key );
+ }
+ catch ( MissingResourceException mre )
+ {
+ // ignore
+ }
+
+ // now check the default resource bundle
+ try
+ {
+ return defaultResourceBundle.getObject( key );
+ }
+ catch ( MissingResourceException mre )
+ {
+ // ignore
+ }
+
+ // finally fall back to using the key
+ return key;
+ }
+
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ConsolePropertyResourceBundle.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ConsolePropertyResourceBundle.java
new file mode 100644
index 0000000..e73d482
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ConsolePropertyResourceBundle.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.i18n;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.ResourceBundle;
+
+
+class ConsolePropertyResourceBundle extends ResourceBundle
+{
+
+ private final Properties props;
+
+
+ ConsolePropertyResourceBundle( final ResourceBundle parent, final URL source )
+ {
+ setParent( parent );
+
+ props = new Properties();
+ if ( source != null )
+ {
+ InputStream ins = null;
+ try
+ {
+ ins = source.openStream();
+ props.load( ins );
+ }
+ catch ( IOException ignore )
+ {
+ }
+ finally
+ {
+ if ( ins != null )
+ {
+ try
+ {
+ ins.close();
+ }
+ catch ( IOException ignore )
+ {
+ }
+ }
+ }
+
+ }
+ }
+
+
+ public Enumeration getKeys()
+ {
+ return new CombinedEnumeration( props.keys(), parent.getKeys() );
+ }
+
+
+ protected Object handleGetObject( String key )
+ {
+ return props.get( key );
+ }
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java
new file mode 100644
index 0000000..5bf3f91
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java
@@ -0,0 +1,164 @@
+/*
+ * 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.i18n;
+
+
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+
+/**
+ * The <code>ResourceBundleCache</code> caches resource bundles per OSGi bundle.
+ */
+class ResourceBundleCache
+{
+
+ private static final Locale DEFAULT_LOCALE = new Locale( "" );
+
+ private final Bundle bundle;
+
+ private final Map resourceBundles;
+
+ private Map resourceBundleEntries;
+
+
+ ResourceBundleCache( final Bundle bundle )
+ {
+ this.bundle = bundle;
+ this.resourceBundles = new HashMap();
+ }
+
+
+ ResourceBundle getResourceBundle( final Locale locale )
+ {
+ if ( locale == null )
+ {
+ return getResourceBundleInternal( DEFAULT_LOCALE );
+ }
+
+ return getResourceBundleInternal( locale );
+ }
+
+
+ ResourceBundle getResourceBundleInternal( final Locale locale )
+ {
+ if ( locale == null )
+ {
+ return null;
+ }
+
+ synchronized ( resourceBundles )
+ {
+ ResourceBundle bundle = ( ResourceBundle ) resourceBundles.get( locale );
+ if ( bundle != null )
+ {
+ return bundle;
+ }
+ }
+
+ ResourceBundle parent = getResourceBundleInternal( getParentLocale( locale ) );
+ ResourceBundle bundle = loadResourceBundle( parent, locale );
+ synchronized ( resourceBundles )
+ {
+ resourceBundles.put( locale, bundle );
+ }
+
+ return bundle;
+ }
+
+
+ private ResourceBundle loadResourceBundle( final ResourceBundle parent, final Locale locale )
+ {
+ final String path = "_" + locale.toString();
+ final URL source = ( URL ) getResourceBundleEntries().get( path );
+ return new ConsolePropertyResourceBundle( parent, source );
+ }
+
+
+ private synchronized Map getResourceBundleEntries()
+ {
+ if ( this.resourceBundleEntries == null )
+ {
+ String file = ( String ) bundle.getHeaders().get( Constants.BUNDLE_LOCALIZATION );
+ if ( file == null )
+ {
+ file = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME;
+ }
+
+ // remove leading slash
+ if ( file.startsWith( "/" ) )
+ {
+ file = file.substring( 1 );
+ }
+
+ // split path and base name
+ int slash = file.lastIndexOf( '/' );
+ String fileName = file.substring( slash + 1 );
+ String path = ( slash <= 0 ) ? "/" : file.substring( 0, slash );
+
+ HashMap resourceBundleEntries = new HashMap();
+
+ Enumeration locales = bundle.findEntries( path, fileName + "*.properties", false );
+ while ( locales.hasMoreElements() )
+ {
+ URL entry = ( URL ) locales.nextElement();
+
+ // calculate the key
+ String entryPath = entry.getPath();
+ final int start = 1 + file.length(); // leading slash
+ final int end = entryPath.length() - 11; // .properties suffix
+ entryPath = entryPath.substring( start, end );
+
+ resourceBundleEntries.put( entryPath, entry );
+ }
+
+ this.resourceBundleEntries = resourceBundleEntries;
+ }
+
+ return this.resourceBundleEntries;
+ }
+
+
+ private Locale getParentLocale( Locale locale )
+ {
+ if ( locale.getVariant().length() != 0 )
+ {
+ return new Locale( locale.getLanguage(), locale.getCountry() );
+ }
+ else if ( locale.getCountry().length() != 0 )
+ {
+ return new Locale( locale.getLanguage() );
+ }
+ else if ( locale.getLanguage().equals( DEFAULT_LOCALE.getLanguage() ) )
+ {
+ return DEFAULT_LOCALE;
+ }
+
+ // no more parents
+ return null;
+ }
+
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleManager.java
new file mode 100644
index 0000000..825e99b
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleManager.java
@@ -0,0 +1,99 @@
+/*
+ * 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.i18n;
+
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+
+
+public class ResourceBundleManager implements BundleListener
+{
+
+ private final BundleContext bundleContext;
+
+ private final ResourceBundleCache consoleResourceBundleCache;
+
+ private final Map resourceBundleCaches;
+
+
+ public ResourceBundleManager( final BundleContext bundleContext )
+ {
+ this.bundleContext = bundleContext;
+ this.consoleResourceBundleCache = new ResourceBundleCache( bundleContext.getBundle() );
+ this.resourceBundleCaches = new HashMap();
+
+ bundleContext.addBundleListener( this );
+ }
+
+
+ public void dispose()
+ {
+ bundleContext.removeBundleListener( this );
+ }
+
+
+ public ResourceBundle getResourceBundle( final Bundle provider, final Locale locale )
+ {
+ // check whether we have to return the resource bundle for the
+ // Web Console itself in which case we directly return it
+ final ResourceBundle defaultResourceBundle = consoleResourceBundleCache.getResourceBundle( locale );
+ if ( provider == null || provider.equals( bundleContext.getBundle() ) )
+ {
+ return defaultResourceBundle;
+ }
+
+ ResourceBundleCache cache;
+ synchronized ( resourceBundleCaches )
+ {
+ Long key = new Long( provider.getBundleId() );
+ cache = ( ResourceBundleCache ) resourceBundleCaches.get( key );
+ if ( cache == null )
+ {
+ cache = new ResourceBundleCache( provider );
+ resourceBundleCaches.put( key, cache );
+ }
+ }
+
+ final ResourceBundle bundleResourceBundle = cache.getResourceBundle( locale );
+ return new CombinedResourceBundle( bundleResourceBundle, defaultResourceBundle );
+ }
+
+
+ // ---------- BundleListener
+
+ public void bundleChanged( BundleEvent event )
+ {
+ if ( event.getType() == BundleEvent.STOPPED )
+ {
+ Long key = new Long( event.getBundle().getBundleId() );
+ synchronized ( resourceBundleCaches )
+ {
+ resourceBundleCaches.remove( key );
+ }
+ }
+ }
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 4d51a71..8f245bf 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -18,16 +18,42 @@
import java.io.IOException;
-import java.util.*;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
-import javax.servlet.*;
+import javax.servlet.GenericServlet;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.apache.felix.webconsole.*;
-import org.apache.felix.webconsole.internal.*;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.Action;
+import org.apache.felix.webconsole.BrandingPlugin;
+import org.apache.felix.webconsole.VariableResolver;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.internal.Logger;
+import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
+import org.apache.felix.webconsole.internal.Util;
+import org.apache.felix.webconsole.internal.WebConsolePluginAdapter;
import org.apache.felix.webconsole.internal.core.BundlesServlet;
-import org.osgi.framework.*;
+import org.apache.felix.webconsole.internal.filter.FilteringResponseWrapper;
+import org.apache.felix.webconsole.internal.i18n.ResourceBundleManager;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.log.LogService;
@@ -35,7 +61,9 @@
/**
- * The <code>OSGi Manager</code> TODO
+ * The <code>OSGi Manager</code> is the actual Web Console Servlet which
+ * is registered with the OSGi Http Service and which maintains registered
+ * console plugins.
*/
public class OsgiManager extends GenericServlet
{
@@ -133,13 +161,17 @@
private ServiceRegistration configurationListener;
+ // map of plugins: indexed by the plugin label (String), values are
+ // AbstractWebConsolePlugin instances
private Map plugins = new HashMap();
+ // map of labels to plugin titles: indexed by plugin label (String, values
+ // are plugin titles
private Map labelMap = new HashMap();
private Map operations = new HashMap();
- private Servlet defaultRender;
+ private AbstractWebConsolePlugin defaultPlugin;
private String defaultRenderName;
@@ -155,6 +187,7 @@
private Set enabledPlugins;
+ private ResourceBundleManager resourceBundleManager;
public OsgiManager( BundleContext bundleContext )
{
@@ -201,7 +234,7 @@
log.dispose();
}
- this.defaultRender = null;
+ this.defaultPlugin = null;
this.bundleContext = null;
}
@@ -237,8 +270,7 @@
}
if ( plugin instanceof AbstractWebConsolePlugin )
{
- AbstractWebConsolePlugin amp = ( AbstractWebConsolePlugin ) plugin;
- bindServlet( amp.getLabel(), amp );
+ bindServlet( ( AbstractWebConsolePlugin ) plugin );
}
else
{
@@ -275,6 +307,9 @@
pluginsTracker.open();
brandingTracker = new BrandingServiceTracker(this);
brandingTracker.open();
+
+ // the resource bundle manager
+ resourceBundleManager = new ResourceBundleManager( getBundleContext() );
}
public void service( ServletRequest req, ServletResponse res ) throws ServletException, IOException
@@ -310,7 +345,7 @@
}
label = label.substring( 1, slash );
- Servlet plugin = ( Servlet ) plugins.get( label );
+ AbstractWebConsolePlugin plugin = ( AbstractWebConsolePlugin ) plugins.get( label );
if ( plugin != null )
{
// the official request attributes
@@ -322,7 +357,10 @@
req.setAttribute( ATTR_LABEL_MAP_OLD, labelMap );
req.setAttribute( ATTR_APP_ROOT_OLD, request.getContextPath() + request.getServletPath() );
- plugin.service( req, res );
+ // wrap the response for localization and template variable replacement
+ response = wrapResponse( request, response, plugin );
+
+ plugin.service( request, response );
}
else
{
@@ -337,6 +375,13 @@
// base class destroy not needed, since the GenericServlet.destroy
// is an empty method
+ // dispose off the resource bundle manager
+ if ( resourceBundleManager != null )
+ {
+ resourceBundleManager.dispose();
+ resourceBundleManager = null;
+ }
+
// stop listening for plugins
if ( operationsTracker != null )
{
@@ -428,6 +473,16 @@
return bundleContext;
}
+
+ private HttpServletResponse wrapResponse( final HttpServletRequest request, final HttpServletResponse response,
+ final AbstractWebConsolePlugin plugin )
+ {
+ final Locale locale = request.getLocale();
+ final ResourceBundle resourceBundle = resourceBundleManager.getResourceBundle( plugin.getBundle(), locale );
+ final VariableResolver variables = ( plugin instanceof VariableResolver ) ? ( VariableResolver ) plugin : null;
+ return new FilteringResponseWrapper( response, resourceBundle, variables );
+ }
+
private static class HttpServiceTracker extends ServiceTracker
{
@@ -521,25 +576,39 @@
{
// wrap the servlet if it is not an AbstractWebConsolePlugin
// but has a title in the service properties
- if ( !( operation instanceof AbstractWebConsolePlugin ) )
+ final AbstractWebConsolePlugin plugin;
+ if ( operation instanceof AbstractWebConsolePlugin )
{
+ plugin = ( AbstractWebConsolePlugin ) operation;
+ }
+ else
+ {
+
+ // define the title from the PLUGIN_TITLE registration
+ // property, the servlet name or the servlet "toString"
Object title = reference.getProperty( WebConsoleConstants.PLUGIN_TITLE );
- if ( title instanceof String )
+ if ( !( title instanceof String ) )
{
- WebConsolePluginAdapter pluginAdapter = new WebConsolePluginAdapter( ( String ) label,
- ( String ) title, ( Servlet ) operation, reference );
+ if ( operation instanceof GenericServlet )
+ {
+ title = ( ( GenericServlet ) operation ).getServletName();
+ }
- // ensure the AbstractWebConsolePlugin is correctly setup
- Bundle pluginBundle = reference.getBundle();
- pluginAdapter.activate( pluginBundle.getBundleContext() );
-
- // now use this adapter as the operation
- operation = pluginAdapter;
+ if ( !( title instanceof String ) )
+ {
+ title = operation.toString();
+ }
}
+
+ plugin = new WebConsolePluginAdapter( ( String ) label, ( String ) title,
+ ( Servlet ) operation, reference );
+
+ // ensure the AbstractWebConsolePlugin is correctly setup
+ Bundle pluginBundle = reference.getBundle();
+ plugin.activate( pluginBundle.getBundleContext() );
}
- // TODO: check reference properties !!
- osgiManager.bindServlet( ( String ) label, ( Servlet ) operation );
+ osgiManager.bindServlet( plugin );
}
return operation;
}
@@ -680,58 +749,53 @@
}
- protected void bindServlet( String label, Servlet servlet )
+ private void bindServlet( final AbstractWebConsolePlugin plugin )
{
+ final String label = plugin.getLabel();
+ final String title = plugin.getTitle();
try
{
- servlet.init( getServletConfig() );
- plugins.put( label, servlet );
+ plugin.init( getServletConfig() );
+ plugins.put( label, plugin );
+ labelMap.put( label, title );
- if ( servlet instanceof GenericServlet )
+ if ( this.defaultPlugin == null )
{
- String title = ( ( GenericServlet ) servlet ).getServletName();
- if ( title != null )
- {
- labelMap.put( label, title );
- }
- }
-
- if ( this.defaultRender == null )
- {
- this.defaultRender = servlet;
+ this.defaultPlugin = plugin;
}
else if ( label.equals( this.defaultRenderName ) )
{
- this.defaultRender = servlet;
+ this.defaultPlugin = plugin;
}
}
catch ( ServletException se )
{
- // TODO: log
+ log.log( LogService.LOG_WARNING, "Initialization of plugin '" + title + "' (" + label
+ + ") failed; not using this plugin", se );
}
}
- protected void unbindServlet( String label )
+ private void unbindServlet( String label )
{
- Servlet servlet = ( Servlet ) plugins.remove( label );
- if ( servlet != null )
+ AbstractWebConsolePlugin plugin = ( AbstractWebConsolePlugin ) plugins.remove( label );
+ if ( plugin != null )
{
labelMap.remove( label );
- if ( this.defaultRender == servlet )
+ if ( this.defaultPlugin == plugin )
{
if ( this.plugins.isEmpty() )
{
- this.defaultRender = null;
+ this.defaultPlugin = null;
}
else
{
- this.defaultRender = ( Servlet ) plugins.values().iterator().next();
+ this.defaultPlugin = ( AbstractWebConsolePlugin ) plugins.values().iterator().next();
}
}
- servlet.destroy();
+ plugin.destroy();
}
}
@@ -766,7 +830,7 @@
defaultRenderName = getProperty( config, PROP_DEFAULT_RENDER, DEFAULT_PAGE );
if ( defaultRenderName != null && plugins.get( defaultRenderName ) != null )
{
- defaultRender = ( Servlet ) plugins.get( defaultRenderName );
+ defaultPlugin = ( AbstractWebConsolePlugin ) plugins.get( defaultRenderName );
}
// get the web manager root path