Ongoing work to add client profile and run browser inside it.
diff --git a/browser-module/pom.xml b/browser-module/pom.xml
index 63e6d94..593d5ed 100644
--- a/browser-module/pom.xml
+++ b/browser-module/pom.xml
@@ -32,16 +32,9 @@
 

     <dependencies>

         <dependency>

-            <groupId>org.apache.river</groupId>

+            <groupId>org.apache.river.container</groupId>

             <artifactId>browser</artifactId>

-            <version>2.2.2</version>

-            <scope>compile</scope>

-        </dependency>

-

-        <dependency>

-            <groupId>org.apache.river</groupId>

-            <artifactId>browser-dl</artifactId>

-            <version>2.2.2</version>

+            <version>1.0-SNAPSHOT</version>

             <scope>compile</scope>

         </dependency>

 

diff --git a/browser-module/src/assemble/module.xml b/browser-module/src/assemble/module.xml
index e85ea9b..a456cfd 100644
--- a/browser-module/src/assemble/module.xml
+++ b/browser-module/src/assemble/module.xml
@@ -27,7 +27,7 @@
     </formats>
     <includeBaseDirectory>false</includeBaseDirectory>
 
-  <fileSets>
+    <fileSets>
 
         <fileSet>
             <directory>${basedir}/src/main/root</directory>
@@ -38,24 +38,17 @@
             <outputDirectory>/</outputDirectory>
         </fileSet>
   
-  </fileSets>    
-  <dependencySets>
+    </fileSets>    
+    <dependencySets>
      
         <dependencySet>
             <useProjectArtifact>false</useProjectArtifact>
             <outputDirectory>/lib</outputDirectory>
             <includes>
-                <include>org.apache.river:browser</include>
+                <include>*:browser</include>
             </includes>
         </dependencySet>
         
-        <dependencySet>
-            <useProjectArtifact>false</useProjectArtifact>
-            <outputDirectory>/lib-dl</outputDirectory>
-            <includes>
-                <include>org.apache.river:browser-dl</include>
-            </includes>
-        </dependencySet>
     </dependencySets>
 
 </assembly>
diff --git a/browser-module/src/main/root/browser.config b/browser-module/src/main/root/browser.config
index b5fea76..b612279 100644
--- a/browser-module/src/main/root/browser.config
+++ b/browser-module/src/main/root/browser.config
@@ -6,7 +6,7 @@
 import net.jini.jeri.tcp.TcpServerEndpoint;
 import net.jini.security.BasicProxyPreparer;
 
-com.sun.jini.example.browser {
+org.apache.river.container.examples.browser {
 	
     initialLookupGroups = new String[] {$discoveryGroup};
     initialMemberGroups = new String[] {$discoveryGroup};
diff --git a/browser-module/src/main/root/start.properties b/browser-module/src/main/root/start.properties
index f2d75ce..d62b147 100644
--- a/browser-module/src/main/root/start.properties
+++ b/browser-module/src/main/root/start.properties
@@ -16,5 +16,5 @@
  # limitations under the License.
  #
 
-startClass=com.sun.jini.example.browser.Browser
+startClass=org.apache.river.container.examples.browser.Browser
 startParameters=browser.config $*
diff --git a/browser/pom.xml b/browser/pom.xml
new file mode 100644
index 0000000..8870fbe
--- /dev/null
+++ b/browser/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.river.container</groupId>
+        <artifactId>river-container</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <groupId>org.apache.river.container</groupId>
+    <artifactId>browser</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <name>browser</name>
+    <url>http://maven.apache.org</url>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>3.8.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.jini</groupId>
+            <artifactId>jsk-platform</artifactId>
+            <version>2.2.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.jini</groupId>
+            <artifactId>jsk-lib</artifactId>
+            <version>2.2.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.jini</groupId>
+            <artifactId>jsk-resources</artifactId>
+            <version>2.2.1</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/App.java b/browser/src/main/java/org/apache/river/container/examples/browser/App.java
new file mode 100644
index 0000000..0f7400f
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/App.java
@@ -0,0 +1,13 @@
+package org.apache.river.container.examples.browser;
+
+/**
+ * Hello world!
+ *
+ */
+public class App 
+{
+    public static void main( String[] args )
+    {
+        System.out.println( "Hello World!" );
+    }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/Browser.java b/browser/src/main/java/org/apache/river/container/examples/browser/Browser.java
new file mode 100644
index 0000000..16933fb
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/Browser.java
@@ -0,0 +1,1765 @@
+/*
+ * 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.river.container.examples.browser;
+
+import com.sun.jini.admin.DestroyAdmin;
+import com.sun.jini.config.Config;
+import com.sun.jini.logging.Levels;
+import com.sun.jini.proxy.BasicProxyTrustVerifier;
+import com.sun.jini.start.LifeCycle;
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.rmi.server.ExportException;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.Vector;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.swing.BorderFactory;
+import javax.swing.DefaultListModel;
+import javax.swing.Icon;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.ListCellRenderer;
+import javax.swing.SwingUtilities;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.MenuEvent;
+import javax.swing.event.MenuListener;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+import net.jini.admin.Administrable;
+import net.jini.admin.JoinAdmin;
+import net.jini.config.Configuration;
+import net.jini.config.ConfigurationException;
+import net.jini.config.ConfigurationProvider;
+import net.jini.config.EmptyConfiguration;
+import net.jini.config.NoSuchEntryException;
+import net.jini.core.constraint.MethodConstraints;
+import net.jini.core.discovery.LookupLocator;
+import net.jini.core.entry.Entry;
+import net.jini.core.event.EventRegistration;
+import net.jini.core.event.RemoteEvent;
+import net.jini.core.event.RemoteEventListener;
+import net.jini.core.lease.Lease;
+import net.jini.core.lookup.ServiceID;
+import net.jini.core.lookup.ServiceItem;
+import net.jini.core.lookup.ServiceMatches;
+import net.jini.core.lookup.ServiceRegistrar;
+import net.jini.core.lookup.ServiceTemplate;
+import net.jini.discovery.Constants;
+import net.jini.discovery.ConstrainableLookupLocator;
+import net.jini.discovery.DiscoveryEvent;
+import net.jini.discovery.DiscoveryGroupManagement;
+import net.jini.discovery.DiscoveryListener;
+import net.jini.discovery.DiscoveryLocatorManagement;
+import net.jini.discovery.DiscoveryManagement;
+import net.jini.discovery.LookupDiscoveryManager;
+import net.jini.export.Exporter;
+import net.jini.jeri.BasicILFactory;
+import net.jini.jeri.BasicJeriExporter;
+import net.jini.jeri.tcp.TcpServerEndpoint;
+import net.jini.lease.LeaseListener;
+import net.jini.lease.LeaseRenewalEvent;
+import net.jini.lease.LeaseRenewalManager;
+import net.jini.lookup.DiscoveryAdmin;
+import net.jini.lookup.ui.factory.JFrameFactory;
+import net.jini.lookup.entry.UIDescriptor;
+import net.jini.security.BasicProxyPreparer;
+import net.jini.security.ProxyPreparer;
+import net.jini.security.Security;
+import net.jini.security.SecurityContext;
+import net.jini.security.TrustVerifier;
+import net.jini.security.proxytrust.ServerProxyTrust;
+import net.jini.space.JavaSpace05;
+import net.jini.space.JavaSpace;
+
+/*
+ * This is not great user interface design. It was a quick-and-dirty hack
+ * and an experiment in on-the-fly menu construction, and it's still
+ * here because we've never had time to do anything better.
+ */
+/**
+ * Example service browser. See the package documentation for details.
+ *
+ * @author Sun Microsystems, Inc.
+ */
+public class Browser extends JFrame {
+
+    static final String BROWSER = "org.apache.river.container.examples.browser";
+    static final Logger logger = Logger.getLogger(BROWSER);
+    private transient SecurityContext ctx;
+    private transient ClassLoader ccl;
+    transient Configuration config;
+    private transient DiscoveryGroupManagement disco;
+    private transient ServiceRegistrar lookup = null;
+    private transient Object eventSource = null;
+    private transient long eventID = 0;
+    private transient long seqNo = Long.MAX_VALUE;
+    private transient ActionListener exiter;
+    private transient ServiceTemplate tmpl;
+    private transient Listener listen;
+    private transient LookupListener adder;
+    private transient Lease elease = null;
+    transient ProxyPreparer leasePreparer;
+    transient ProxyPreparer servicePreparer;
+    transient ProxyPreparer adminPreparer;
+    private transient MethodConstraints locatorConstraints;
+    transient LeaseRenewalManager leaseMgr;
+    private transient LeaseListener lnotify;
+    private transient List ignoreInterfaces;
+    private transient JTextArea text;
+    private transient JMenu registrars;
+    private transient JCheckBoxMenuItem esuper;
+    private transient JCheckBoxMenuItem ssuper;
+    private transient JCheckBoxMenuItem sclass;
+    private transient boolean isAdmin;
+    private volatile transient boolean autoConfirm;
+    private transient JList list;
+    private transient DefaultListModel listModel;
+    private transient DefaultListModel dummyModel = new DefaultListModel();
+    private transient JScrollPane listScrollPane;
+
+    /**
+     * Creates an instance with the given action listener for the Exit menu item
+     * and the given configuration. The action listener defaults to an instance
+     * of {@link Exit Exit}. The action listener can be overridden by a
+     * configuration entry. The configuration defaults to an empty
+     * configuration.
+     *
+     * @param exiter the action listener, or <code>null</code>
+     * @param config the configuration, or <code>null</code>
+     */
+    public Browser(ActionListener exiter, Configuration config)
+            throws ConfigurationException, IOException {
+        if (exiter == null) {
+            exiter = new Exit();
+        }
+        if (config == null) {
+            config = EmptyConfiguration.INSTANCE;
+        }
+        init(exiter, config);
+    }
+
+    private void init(ActionListener exiter, Configuration config)
+            throws ConfigurationException, IOException {
+        exiter = wrap((ActionListener) Config.getNonNullEntry(config, BROWSER, "exitActionListener",
+                ActionListener.class, exiter));
+        this.exiter = exiter;
+        this.config = config;
+        ctx = Security.getContext();
+        ccl = Thread.currentThread().getContextClassLoader();
+        leaseMgr = (LeaseRenewalManager) Config.getNonNullEntry(config, BROWSER, "leaseManager",
+                LeaseRenewalManager.class,
+                new LeaseRenewalManager(config));
+        isAdmin = ((Boolean) config.getEntry(
+                BROWSER, "folderView",
+                boolean.class, Boolean.TRUE)).booleanValue();
+        leasePreparer = (ProxyPreparer) Config.getNonNullEntry(config, BROWSER, "leasePreparer",
+                ProxyPreparer.class,
+                new BasicProxyPreparer());
+        servicePreparer = (ProxyPreparer) Config.getNonNullEntry(config, BROWSER, "servicePreparer",
+                ProxyPreparer.class,
+                new BasicProxyPreparer());
+        adminPreparer = (ProxyPreparer) Config.getNonNullEntry(config, BROWSER, "adminPreparer",
+                ProxyPreparer.class,
+                new BasicProxyPreparer());
+        locatorConstraints = (MethodConstraints) config.getEntry(BROWSER, "locatorConstraints",
+                MethodConstraints.class, null);
+        ignoreInterfaces = Arrays.asList((String[]) Config.getNonNullEntry(config, BROWSER, "uninterestingInterfaces",
+                String[].class,
+                new String[]{
+            "java.io.Serializable",
+            "java.rmi.Remote",
+            "net.jini.admin.Administrable",
+            "net.jini.core.constraint.RemoteMethodControl",
+            "net.jini.id.ReferentUuid",
+            "net.jini.security.proxytrust.TrustEquivalence"}));
+        autoConfirm = ((Boolean) config.getEntry(
+                BROWSER, "autoConfirm", boolean.class,
+                Boolean.FALSE)).booleanValue();
+        listen = new Listener();
+        try {
+            DiscoveryManagement disco = (DiscoveryManagement) Config.getNonNullEntry(config, BROWSER, "discoveryManager",
+                    DiscoveryManagement.class);
+            if (!(disco instanceof DiscoveryGroupManagement)) {
+                throw new ConfigurationException(
+                        "discoveryManager does not "
+                        + " support DiscoveryGroupManagement");
+            } else if (!(disco instanceof DiscoveryLocatorManagement)) {
+                throw new ConfigurationException(
+                        "discoveryManager does not "
+                        + " support DiscoveryLocatorManagement");
+            }
+            this.disco = (DiscoveryGroupManagement) disco;
+            String[] groups = this.disco.getGroups();
+            if (groups == null || groups.length > 0) {
+                throw new ConfigurationException(
+                        "discoveryManager cannot have initial groups");
+            }
+            if (((DiscoveryLocatorManagement) disco).getLocators().length > 0) {
+                throw new ConfigurationException(
+                        "discoveryManager cannot have initial locators");
+            }
+        } catch (NoSuchEntryException e) {
+            disco = new LookupDiscoveryManager(new String[0],
+                    new LookupLocator[0], null,
+                    config);
+        }
+        disco.setGroups((String[]) config.getEntry(BROWSER,
+                "initialLookupGroups",
+                String[].class,
+                null));
+        ((DiscoveryLocatorManagement) disco).setLocators((LookupLocator[]) Config.getNonNullEntry(config, BROWSER, "initialLookupLocators",
+                LookupLocator[].class,
+                new LookupLocator[0]));
+        tmpl = new ServiceTemplate(null, new Class[0], new Entry[0]);
+        setTitle("Service Browser");
+        JMenuBar bar = new JMenuBar();
+        JMenu file = new JMenu("File");
+        JMenuItem allfind = new JMenuItem("Find All");
+        allfind.addActionListener(wrap(new AllFind()));
+        file.add(allfind);
+        JMenuItem pubfind = new JMenuItem("Find Public");
+        pubfind.addActionListener(wrap(new PubFind()));
+        file.add(pubfind);
+        JMenuItem multifind = new JMenuItem("Find By Group...");
+        multifind.addActionListener(wrap(new MultiFind()));
+        file.add(multifind);
+        JMenuItem unifind = new JMenuItem("Find By Address...");
+        unifind.addActionListener(wrap(new UniFind()));
+        file.add(unifind);
+        if (!isAdmin) {
+            JMenuItem show = new JMenuItem("Show Matches");
+            show.addActionListener(wrap(new Show()));
+            file.add(show);
+        }
+        JMenuItem reset = new JMenuItem("Reset");
+        reset.addActionListener(wrap(new Reset()));
+        file.add(reset);
+        JMenuItem exit = new JMenuItem("Exit");
+        exit.addActionListener(exiter);
+        file.add(exit);
+        bar.add(file);
+        addWindowListener(new Exiter());
+        registrars = new JMenu("Registrar");
+        addNone(registrars);
+        bar.add(registrars);
+        JMenu options = new JMenu("Options");
+        esuper = new JCheckBoxMenuItem("Attribute supertypes", false);
+        options.add(esuper);
+        ssuper = new JCheckBoxMenuItem("Service supertypes", false);
+        options.add(ssuper);
+        sclass = new JCheckBoxMenuItem("Service classes", false);
+        options.add(sclass);
+        bar.add(options);
+        JMenu services = new JMenu("Services");
+        services.addMenuListener(wrap(new Services(services)));
+        bar.add(services);
+        JMenu attrs = new JMenu("Attributes");
+        attrs.addMenuListener(wrap(new Entries(attrs)));
+        bar.add(attrs);
+        setJMenuBar(bar);
+
+        getContentPane().setLayout(new BorderLayout());
+        int textRows = 8;
+        if (isAdmin) {
+            textRows = 4;
+            JPanel bpanel = new JPanel();
+            bpanel.setLayout(new BorderLayout());
+
+            TitledBorder border = BorderFactory.createTitledBorder("Matching Services");
+            border.setTitlePosition(TitledBorder.TOP);
+            border.setTitleJustification(TitledBorder.LEFT);
+            bpanel.setBorder(border);
+
+            listModel = new DefaultListModel();
+            list = new JList(listModel);
+            list.setFixedCellHeight(20);
+            list.setCellRenderer((ListCellRenderer) wrap(new ServiceItemRenderer(), ListCellRenderer.class));
+            list.addMouseListener(
+                    wrap(new MouseReceiver(new ServiceListPopup())));
+            listScrollPane = new JScrollPane(list);
+            bpanel.add(listScrollPane, "Center");
+            getContentPane().add(bpanel, "South");
+        }
+        text = new JTextArea(genText(false), textRows, 40);
+        text.setEditable(false);
+        JScrollPane scroll = new JScrollPane(text);
+        getContentPane().add(scroll, "Center");
+
+        validate();
+        SwingUtilities.invokeLater(wrap(new Runnable() {
+            public void run() {
+                pack();
+                show();
+            }
+        }));
+        adder = new LookupListener();
+        lnotify = new LeaseNotify();
+        ((DiscoveryManagement) disco).addDiscoveryListener(adder);
+    }
+
+    /**
+     * Creates an instance with the given command line arguments and life cycle
+     * callback. See the package documentation for details of the command line
+     * arguments. The default action listener for the Exit menu item calls the
+     * {@link #dispose dispose} method of this instance, cancels any lookup
+     * service event registration lease, unexports any remote event listener,
+     * and calls the {@link LifeCycle#unregister unregister} method of the life
+     * cycle callback. The action listener can be overridden by a configuration
+     * entry.
+     *
+     * @param args command line arguments
+     * @param lc life cycle callback, or <code>null</code>.
+     */
+    public Browser(String[] args, final LifeCycle lc)
+            throws ConfigurationException, LoginException, IOException {
+        final ActionListener exiter = new ActionListener() {
+            public void actionPerformed(ActionEvent ev) {
+                Browser.this.dispose();
+                cancelLease();
+                listen.unexport();
+                if (lc != null) {
+                    lc.unregister(Browser.this);
+                }
+                System.exit(0);
+            }
+        };
+        final Configuration config =
+                ConfigurationProvider.getInstance(
+                args, Browser.class.getClassLoader());
+        LoginContext login =
+                (LoginContext) config.getEntry(BROWSER, "loginContext",
+                LoginContext.class, null);
+        if (login == null) {
+            init(exiter, config);
+        } else {
+            login.login();
+            try {
+                Subject.doAsPrivileged(
+                        login.getSubject(),
+                        new PrivilegedExceptionAction() {
+                    public Object run()
+                            throws ConfigurationException, IOException {
+                        init(exiter, config);
+                        return null;
+                    }
+                },
+                        null);
+            } catch (PrivilegedActionException pae) {
+                Exception e = pae.getException();
+                if (e instanceof ConfigurationException) {
+                    throw (ConfigurationException) e;
+                }
+                throw (IOException) e;
+            }
+        }
+    }
+
+    private static String typeName(Class type) {
+        String name = type.getName();
+        int i = name.lastIndexOf('.');
+        if (i >= 0) {
+            name = name.substring(i + 1);
+        }
+        return name;
+    }
+
+    private void setText(boolean match) {
+        text.setText(genText(match));
+    }
+
+    private String genText(boolean match) {
+        StringBuffer buf = new StringBuffer();
+        if (tmpl.serviceTypes.length > 0) {
+            for (int i = 0; i < tmpl.serviceTypes.length; i++) {
+                buf.append(tmpl.serviceTypes[i].getName());
+                buf.append("\n");
+            }
+        }
+        if (tmpl.attributeSetTemplates.length > 0) {
+            genEntries(buf, tmpl.attributeSetTemplates, false);
+        }
+        genMatches(buf, match);
+        return buf.toString();
+    }
+
+    private void genEntries(StringBuffer buf,
+            Entry[] entries,
+            boolean showNulls) {
+        for (int i = 0; i < entries.length; i++) {
+            Entry ent = entries[i];
+            if (ent == null) {
+                buf.append("null\n");
+                continue;
+            }
+            buf.append(typeName(ent.getClass()));
+            buf.append(": ");
+            try {
+                Field[] fields = ent.getClass().getFields();
+                for (int j = 0; j < fields.length; j++) {
+                    if (!valid(fields[j])) {
+                        continue;
+                    }
+                    Object val = fields[j].get(ent);
+                    if (val != null || showNulls) {
+                        buf.append(fields[j].getName());
+                        buf.append("=");
+                        buf.append(val);
+                        buf.append(" ");
+                    }
+                }
+            } catch (Exception e) {
+            }
+            buf.append("\n");
+        }
+    }
+
+    private static boolean valid(Field f) {
+        return (f.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) == 0;
+    }
+
+    private void genMatches(StringBuffer buf, boolean match) {
+        if (isAdmin) {
+            list.setModel(dummyModel);	// to keep away from Swing's bug
+
+            listModel.removeAllElements();
+            list.clearSelection();
+            list.ensureIndexIsVisible(0);
+            list.repaint();
+            list.revalidate();
+            listScrollPane.validate();
+        }
+        if (lookup == null) {
+            String[] groups = disco.getGroups();
+            if (groups == null) {
+                buf.append("Groups: <all>\n");
+            } else if (groups.length > 0) {
+                buf.append("Groups:");
+                for (int i = 0; i < groups.length; i++) {
+                    String group = groups[i];
+                    if (group.length() == 0) {
+                        group = "public";
+                    }
+                    buf.append(" ");
+                    buf.append(group);
+                }
+                buf.append("\n");
+            }
+            LookupLocator[] locators =
+                    ((DiscoveryLocatorManagement) disco).getLocators();
+            if (locators.length > 0) {
+                buf.append("Addresses:");
+                for (int i = 0; i < locators.length; i++) {
+                    buf.append(" ");
+                    buf.append(locators[i].getHost());
+                    if (locators[i].getPort() != Constants.discoveryPort) {
+                        buf.append(":");
+                        buf.append(locators[i].getPort());
+                    }
+                }
+                buf.append("\n");
+            }
+            if (!(registrars.getMenuComponent(0) instanceof JRadioButtonMenuItem)) {
+                buf.append("No registrars to select");
+                return;
+            }
+            int n = registrars.getMenuComponentCount();
+            if (n == 1) {
+                buf.append("1 registrar, not selected");
+            } else {
+                buf.append(n);
+                buf.append(" registrars, none selected");
+            }
+            return;
+        }
+        ServiceMatches matches;
+        try {
+            matches = lookup.lookup(tmpl, (match || isAdmin) ? 1000 : 0);
+        } catch (Throwable t) {
+            logger.log(Level.INFO, "lookup failed", t);
+            return;
+        }
+
+        if (matches.items != null) {
+            for (int i = 0; i < matches.items.length; i++) {
+                if (matches.items[i].service != null) {
+                    try {
+                        matches.items[i].service =
+                                servicePreparer.prepareProxy(
+                                matches.items[i].service);
+                    } catch (Throwable t) {
+                        logger.log(Level.INFO, "proxy preparation failed", t);
+                        matches.items[i].service = null;
+                    }
+                }
+            }
+        }
+
+        if (isAdmin) {
+            for (int i = 0; i < matches.items.length; i++) {
+                listModel.addElement(new ServiceListItem(matches.items[i]));
+            }
+            list.setModel(listModel);
+            list.clearSelection();
+            list.ensureIndexIsVisible(0);
+            list.repaint();
+            list.revalidate();
+            listScrollPane.validate();
+        }
+
+        if (!match
+                && tmpl.serviceTypes.length == 0
+                && tmpl.attributeSetTemplates.length == 0) {
+            buf.append("Total services registered: ");
+            buf.append(matches.totalMatches);
+            return;
+        }
+        buf.append("\nMatching services: ");
+        buf.append(matches.totalMatches);
+
+        if (!isAdmin) {
+            if (!match) {
+                return;
+            }
+            buf.append("\n\n");
+            for (int i = 0; i < matches.items.length; i++) {
+                ServiceItem item = matches.items[i];
+                buf.append("Service ID: ");
+                buf.append(item.serviceID);
+                buf.append("\n");
+                buf.append("Service instance: ");
+                buf.append(item.service);
+                buf.append("\n");
+                genEntries(buf, item.attributeSets, true);
+                buf.append("\n");
+            }
+        }
+    }
+
+    private static void addNone(JMenu menu) {
+        JMenuItem item = new JMenuItem("(none)");
+        item.setEnabled(false);
+        menu.add(item);
+    }
+
+    private void addOne(ServiceRegistrar registrar) {
+        LookupLocator loc;
+        try {
+            loc = registrar.getLocator();
+        } catch (Throwable t) {
+            logger.log(Level.INFO, "obtaining locator failed", t);
+            return;
+        }
+        String host = loc.getHost();
+        if (loc.getPort() != Constants.discoveryPort) {
+            host += ":" + loc.getPort();
+        }
+        JRadioButtonMenuItem reg =
+                new RegistrarMenuItem(host, registrar.getServiceID());
+        reg.addActionListener(wrap(new Lookup(registrar)));
+        if (!(registrars.getMenuComponent(0) instanceof JRadioButtonMenuItem)) {
+            registrars.removeAll();
+        }
+        registrars.add(reg);
+    }
+
+    private static class RegistrarMenuItem extends JRadioButtonMenuItem {
+
+        ServiceID id;
+
+        RegistrarMenuItem(String host, ServiceID id) {
+            super(host);
+            this.id = id;
+        }
+    }
+
+    static Class[] getInterfaces(Class c) {
+        Set set = new HashSet();
+        for (; c != null; c = c.getSuperclass()) {
+            Class[] ifs = c.getInterfaces();
+            for (int i = ifs.length; --i >= 0;) {
+                set.add(ifs[i]);
+            }
+        }
+        return (Class[]) set.toArray(new Class[set.size()]);
+    }
+
+    private class Services implements MenuListener {
+
+        private JMenu menu;
+
+        public Services(JMenu menu) {
+            this.menu = menu;
+        }
+
+        public void menuSelected(MenuEvent ev) {
+            if (lookup == null) {
+                addNone(menu);
+                return;
+            }
+            Vector all = new Vector();
+            Class[] types = tmpl.serviceTypes;
+            for (int i = 0; i < types.length; i++) {
+                all.addElement(types[i]);
+                JCheckBoxMenuItem item =
+                        new JCheckBoxMenuItem(types[i].getName(), true);
+                item.addActionListener(wrap(new Service(types[i], i)));
+                menu.add(item);
+            }
+            try {
+                types = lookup.getServiceTypes(tmpl, "");
+            } catch (Throwable t) {
+                failure(t);
+                return;
+            }
+            if (types == null) {
+                if (all.isEmpty()) {
+                    addNone(menu);
+                }
+                return;
+            }
+            for (int i = 0; i < types.length; i++) {
+                Class[] stypes;
+                if (types[i] == null) {
+                    all.addElement(new JMenuItem("null"));
+                    continue;
+                }
+                if (types[i].isInterface() || sclass.getState()) {
+                    stypes = new Class[]{types[i]};
+                } else {
+                    stypes = getInterfaces(types[i]);
+                }
+                for (int j = 0; j < stypes.length; j++) {
+                    addType(stypes[j], all);
+                }
+            }
+        }
+
+        private void addType(Class type, Vector all) {
+            if (all.contains(type)) {
+                return;
+            }
+            all.addElement(type);
+            JCheckBoxMenuItem item =
+                    new JCheckBoxMenuItem(type.getName(), false);
+            item.addActionListener(wrap(new Service(type, -1)));
+            menu.add(item);
+            if (!ssuper.getState()) {
+                return;
+            }
+            if (sclass.getState() && type.getSuperclass() != null) {
+                addType(type.getSuperclass(), all);
+            }
+            Class[] stypes = type.getInterfaces();
+            for (int i = 0; i < stypes.length; i++) {
+                addType(stypes[i], all);
+            }
+        }
+
+        public void menuDeselected(MenuEvent ev) {
+            menu.removeAll();
+        }
+
+        public void menuCanceled(MenuEvent ev) {
+            menu.removeAll();
+        }
+    }
+
+    /**
+     * Indicates whether auto confirm is enabled to prevent from the user having
+     * to click the 'Yes' button in the a popup window to confirm a modification
+     * to the service browser pane is allowed to take place as result of a
+     * service being removed, or its lookup attributes being changed.
+     *
+     * @return <code>true</code> in case no popup is required to have the user
+     * confirm the modifications, <code>false</code> otherwise
+     */
+    boolean isAutoConfirm() {
+        return autoConfirm;
+    }
+
+    ActionListener wrap(ActionListener l) {
+        return (ActionListener) wrap((Object) l, ActionListener.class);
+    }
+
+    MenuListener wrap(MenuListener l) {
+        return (MenuListener) wrap((Object) l, MenuListener.class);
+    }
+
+    MouseListener wrap(MouseListener l) {
+        return (MouseListener) wrap((Object) l, MouseListener.class);
+    }
+
+    WindowListener wrap(WindowListener a) {
+        return (WindowListener) wrap((Object) a, WindowListener.class);
+    }
+
+    Runnable wrap(Runnable r) {
+        return (Runnable) wrap((Object) r, Runnable.class);
+    }
+
+    private Object wrap(Object obj, Class iface) {
+        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
+                new Class[]{iface}, new Handler(obj));
+    }
+
+    private class Handler implements InvocationHandler {
+
+        private final Object obj;
+
+        Handler(Object obj) {
+            this.obj = obj;
+        }
+
+        public Object invoke(Object proxy,
+                final Method method,
+                final Object[] args)
+                throws Throwable {
+            if (method.getDeclaringClass() == Object.class) {
+                if ("equals".equals(method.getName())) {
+                    return Boolean.valueOf(proxy == args[0]);
+                } else if ("hashCode".equals(method.getName())) {
+                    return new Integer(System.identityHashCode(proxy));
+                }
+            }
+            try {
+                return AccessController.doPrivileged(
+                        ctx.wrap(new PrivilegedExceptionAction() {
+                    public Object run() throws Exception {
+                        Thread t = Thread.currentThread();
+                        ClassLoader occl = t.getContextClassLoader();
+                        try {
+                            t.setContextClassLoader(ccl);
+                            try {
+                                return method.invoke(obj, args);
+                            } catch (InvocationTargetException e) {
+                                Throwable tt = e.getCause();
+                                if (tt instanceof Error) {
+                                    throw (Error) tt;
+                                }
+                                throw (Exception) tt;
+                            }
+                        } finally {
+                            t.setContextClassLoader(occl);
+                        }
+                    }
+                }), ctx.getAccessControlContext());
+            } catch (PrivilegedActionException e) {
+                throw e.getCause();
+            }
+        }
+    }
+
+    private class Show implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            setText(true);
+        }
+    }
+
+    private void resetTmpl() {
+        tmpl.serviceTypes = new Class[0];
+        tmpl.attributeSetTemplates = new Entry[0];
+        update();
+    }
+
+    private void reset() {
+        ssuper.setState(false);
+        esuper.setState(false);
+        sclass.setState(false);
+        resetTmpl();
+    }
+
+    private class Reset implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            reset();
+        }
+    }
+
+    private class Service implements ActionListener {
+
+        private Class type;
+        private int index;
+
+        public Service(Class type, int index) {
+            this.type = type;
+            this.index = index;
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            int z = tmpl.serviceTypes.length;
+            Class[] newTypes;
+            if (index < 0) {
+                newTypes = new Class[z + 1];
+                System.arraycopy(tmpl.serviceTypes, 0, newTypes, 0, z);
+                newTypes[z] = type;
+            } else {
+                newTypes = new Class[z - 1];
+                System.arraycopy(tmpl.serviceTypes, 0,
+                        newTypes, 0, index);
+                System.arraycopy(tmpl.serviceTypes, index + 1,
+                        newTypes, index, z - index - 1);
+            }
+            tmpl.serviceTypes = newTypes;
+            update();
+        }
+    }
+
+    private class Entries implements MenuListener {
+
+        private JMenu menu;
+
+        public Entries(JMenu menu) {
+            this.menu = menu;
+        }
+
+        public void menuSelected(MenuEvent ev) {
+            if (lookup == null) {
+                addNone(menu);
+                return;
+            }
+            Entry[] attrs = tmpl.attributeSetTemplates;
+            for (int i = 0; i < attrs.length; i++) {
+                Class type = attrs[i].getClass();
+                JMenu item = new JMenu(typeName(type));
+                item.addMenuListener(new Fields(item, i));
+                menu.add(item);
+            }
+            Class[] types;
+            try {
+                types = lookup.getEntryClasses(tmpl);
+            } catch (Throwable t) {
+                failure(t);
+                return;
+            }
+            if (types == null) {
+                if (attrs.length == 0) {
+                    addNone(menu);
+                }
+                return;
+            }
+            Vector all = new Vector();
+            for (int i = 0; i < types.length; i++) {
+                if (types[i] == null) {
+                    menu.add(new JMenuItem("null"));
+                } else {
+                    addType(types[i], all);
+                }
+            }
+        }
+
+        private void addType(Class type, Vector all) {
+            if (all.contains(type)) {
+                return;
+            }
+            all.addElement(type);
+            JCheckBoxMenuItem item =
+                    new JCheckBoxMenuItem(typeName(type), false);
+            item.addActionListener(wrap(new AttrSet(type)));
+            menu.add(item);
+            if (esuper.getState()
+                    && Entry.class.isAssignableFrom(type.getSuperclass())) {
+                addType(type.getSuperclass(), all);
+            }
+        }
+
+        public void menuDeselected(MenuEvent ev) {
+            menu.removeAll();
+        }
+
+        public void menuCanceled(MenuEvent ev) {
+            menu.removeAll();
+        }
+    }
+
+    private class AttrSet implements ActionListener {
+
+        private Class type;
+
+        public AttrSet(Class type) {
+            this.type = type;
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            Entry ent;
+            try {
+                ent = (Entry) type.newInstance();
+            } catch (Throwable t) {
+                logger.log(Level.INFO, "creating entry failed", t);
+                return;
+            }
+            int z = tmpl.attributeSetTemplates.length;
+            Entry[] newSets = new Entry[z + 1];
+            System.arraycopy(tmpl.attributeSetTemplates, 0, newSets, 0, z);
+            newSets[z] = ent;
+            tmpl.attributeSetTemplates = newSets;
+            update();
+        }
+    }
+
+    private class Fields implements MenuListener {
+
+        private JMenu menu;
+        private int index;
+
+        public Fields(JMenu menu, int index) {
+            this.menu = menu;
+            this.index = index;
+        }
+
+        public void menuSelected(MenuEvent ev) {
+            JRadioButtonMenuItem match = new JRadioButtonMenuItem("(match)");
+            match.setSelected(true);
+            match.addActionListener(wrap(new Unmatch(index)));
+            menu.add(match);
+            Entry ent = tmpl.attributeSetTemplates[index];
+            Field[] fields = ent.getClass().getFields();
+            for (int i = 0; i < fields.length; i++) {
+                Field field = fields[i];
+                if (!valid(field)) {
+                    continue;
+                }
+                try {
+                    if (field.get(ent) != null) {
+                        JCheckBoxMenuItem item =
+                                new JCheckBoxMenuItem(field.getName(), true);
+                        item.addActionListener(
+                                wrap(new Value(index, field, null)));
+                        menu.add(item);
+                    } else {
+                        JMenu item = new JMenu(field.getName());
+                        item.addMenuListener(
+                                wrap(new Values(item, index, field)));
+                        menu.add(item);
+                    }
+                } catch (Throwable t) {
+                    logger.log(Level.INFO, "getting fields failed", t);
+                }
+            }
+        }
+
+        public void menuDeselected(MenuEvent ev) {
+            menu.removeAll();
+        }
+
+        public void menuCanceled(MenuEvent ev) {
+            menu.removeAll();
+        }
+    }
+
+    private class Unmatch implements ActionListener {
+
+        private int index;
+
+        public Unmatch(int index) {
+            this.index = index;
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            int z = tmpl.attributeSetTemplates.length;
+            Entry[] newSets = new Entry[z - 1];
+            System.arraycopy(tmpl.attributeSetTemplates, 0,
+                    newSets, 0, index);
+            System.arraycopy(tmpl.attributeSetTemplates, index + 1,
+                    newSets, index, z - index - 1);
+            tmpl.attributeSetTemplates = newSets;
+            update();
+        }
+    }
+
+    private class Values implements MenuListener {
+
+        private JMenu menu;
+        private int index;
+        private Field field;
+
+        public Values(JMenu menu, int index, Field field) {
+            this.menu = menu;
+            this.index = index;
+            this.field = field;
+        }
+
+        public void menuSelected(MenuEvent ev) {
+            Object[] values;
+            try {
+                values = lookup.getFieldValues(tmpl, index, field.getName());
+            } catch (Throwable t) {
+                failure(t);
+                return;
+            }
+            if (values == null) {
+                addNone(menu);
+                return;
+            }
+            for (int i = 0; i < values.length; i++) {
+                JMenuItem item = new JMenuItem(values[i].toString());
+                item.addActionListener(
+                        wrap(new Value(index, field, values[i])));
+                menu.add(item);
+            }
+        }
+
+        public void menuDeselected(MenuEvent ev) {
+            menu.removeAll();
+        }
+
+        public void menuCanceled(MenuEvent ev) {
+            menu.removeAll();
+        }
+    }
+
+    private class Value implements ActionListener {
+
+        private int index;
+        private Field field;
+        private Object value;
+
+        public Value(int index, Field field, Object value) {
+            this.index = index;
+            this.field = field;
+            this.value = value;
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            try {
+                field.set(tmpl.attributeSetTemplates[index], value);
+            } catch (Throwable t) {
+                logger.log(Level.INFO, "setting attribute value failed", t);
+            }
+            update();
+        }
+    }
+
+    private class Listener implements RemoteEventListener, ServerProxyTrust {
+
+        private final Exporter exporter;
+        final RemoteEventListener proxy;
+
+        public Listener() throws ConfigurationException, ExportException {
+            exporter = (Exporter) Config.getNonNullEntry(config, BROWSER, "listenerExporter",
+                    Exporter.class,
+                    new BasicJeriExporter(
+                    TcpServerEndpoint.getInstance(0),
+                    new BasicILFactory(),
+                    false, false));
+            proxy = (RemoteEventListener) exporter.export(this);
+        }
+
+        public void notify(final RemoteEvent ev) {
+            SwingUtilities.invokeLater(wrap(new Runnable() {
+                public void run() {
+                    if (eventID == ev.getID()
+                            && seqNo < ev.getSequenceNumber()
+                            && eventSource != null
+                            && eventSource.equals(ev.getSource())) {
+                        seqNo = ev.getSequenceNumber();
+                        setText(false);
+                    }
+                }
+            }));
+        }
+
+        public TrustVerifier getProxyVerifier() {
+            return new BasicProxyTrustVerifier(proxy);
+        }
+
+        void unexport() {
+            exporter.unexport(true);
+        }
+    }
+
+    private class LookupListener implements DiscoveryListener {
+
+        public void discovered(DiscoveryEvent e) {
+            final ServiceRegistrar[] newregs = e.getRegistrars();
+            SwingUtilities.invokeLater(wrap(new Runnable() {
+                public void run() {
+                    for (int i = 0; i < newregs.length; i++) {
+                        addOne(newregs[i]);
+                    }
+                    if (lookup == null) {
+                        setText(false);
+                    }
+                }
+            }));
+        }
+
+        public void discarded(DiscoveryEvent e) {
+            final ServiceRegistrar[] regs = e.getRegistrars();
+            SwingUtilities.invokeLater(wrap(new Runnable() {
+                public void run() {
+                    for (int i = 0; i < regs.length; i++) {
+                        ServiceID id = regs[i].getServiceID();
+                        if (lookup != null
+                                && id.equals(lookup.getServiceID())) {
+                            lookup = null;
+                            seqNo = Long.MAX_VALUE;
+                        }
+                        for (int j = 0;
+                                j < registrars.getMenuComponentCount();
+                                j++) {
+                            JMenuItem item =
+                                    (JMenuItem) registrars.getMenuComponent(j);
+                            if (item instanceof RegistrarMenuItem
+                                    && id.equals(((RegistrarMenuItem) item).id)) {
+                                item.setSelected(false);
+                                registrars.remove(item);
+                                if (registrars.getMenuComponentCount() == 0) {
+                                    addNone(registrars);
+                                }
+                                break;
+                            }
+                        }
+                    }
+                    if (lookup == null) {
+                        resetTmpl();
+                    }
+                }
+            }));
+        }
+    }
+
+    private void setGroups(String[] groups) {
+        ((DiscoveryLocatorManagement) disco).setLocators(new LookupLocator[0]);
+        try {
+            disco.setGroups(groups);
+        } catch (Throwable t) {
+            logger.log(Level.INFO, "setting groups failed", t);
+        }
+        resetTmpl();
+    }
+
+    private class AllFind implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            setGroups(null);
+        }
+    }
+
+    private class PubFind implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            setGroups(new String[]{""});
+        }
+    }
+
+    private class MultiFind implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            String names = JOptionPane.showInputDialog(Browser.this,
+                    "Enter group names");
+            if (names == null) {
+                return;
+            }
+            setGroups(parseList(names, true));
+        }
+    }
+
+    private class UniFind implements ActionListener {
+
+        public void actionPerformed(ActionEvent ev) {
+            String list =
+                    JOptionPane.showInputDialog(Browser.this,
+                    "Enter host[:port] addresses");
+            if (list == null) {
+                return;
+            }
+            String[] addrs = parseList(list, false);
+            LookupLocator[] locs = new LookupLocator[addrs.length];
+            for (int i = 0; i < addrs.length; i++) {
+                try {
+                    locs[i] = new ConstrainableLookupLocator(
+                            "jini://" + addrs[i], locatorConstraints);
+                } catch (MalformedURLException e) {
+                    JOptionPane.showMessageDialog(Browser.this,
+                            "\"" + addrs[i] + "\": "
+                            + e.getMessage(),
+                            "Bad Address",
+                            JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+            }
+            try {
+                disco.setGroups(new String[0]);
+            } catch (Throwable t) {
+                logger.log(Levels.HANDLED, "setting groups failed", t);
+            }
+            ((DiscoveryLocatorManagement) disco).setLocators(locs);
+            resetTmpl();
+        }
+    }
+
+    private class Lookup implements ActionListener {
+
+        private ServiceRegistrar registrar;
+
+        public Lookup(ServiceRegistrar registrar) {
+            this.registrar = registrar;
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            if (lookup == registrar) {
+                lookup = null;
+            } else {
+                lookup = registrar;
+            }
+            seqNo = Long.MAX_VALUE;
+            for (int i = 0; i < registrars.getMenuComponentCount(); i++) {
+                JMenuItem item = (JMenuItem) registrars.getMenuComponent(i);
+                if (item != ev.getSource()) {
+                    item.setSelected(false);
+                }
+            }
+            resetTmpl();
+        }
+    }
+
+    static class LeaseNotify implements LeaseListener {
+
+        public void notify(LeaseRenewalEvent ev) {
+            if (ev.getException() != null) {
+                logger.log(Level.INFO, "lease renewal failed",
+                        ev.getException());
+            } else {
+                logger.log(Level.INFO, "lease renewal failed");
+            }
+        }
+    }
+
+    private static String[] parseList(String names, boolean groups) {
+        StringTokenizer st = new StringTokenizer(names, " \t\n\r\f,");
+        String[] elts = new String[st.countTokens()];
+        for (int i = 0; st.hasMoreTokens(); i++) {
+            elts[i] = st.nextToken();
+            if (groups && elts[i].equalsIgnoreCase("public")) {
+                elts[i] = "";
+            }
+        }
+        return elts;
+    }
+
+    private void cancelLease() {
+        if (elease != null) {
+            try {
+                leaseMgr.cancel(elease);
+            } catch (Throwable t) {
+                logger.log(Levels.HANDLED, "lease cancellation failed", t);
+            }
+            elease = null;
+            eventSource = null;
+        }
+    }
+
+    private void update() {
+        setText(false);
+        cancelLease();
+        if (lookup == null) {
+            return;
+        }
+        try {
+            EventRegistration reg =
+                    lookup.notify(tmpl,
+                    ServiceRegistrar.TRANSITION_MATCH_NOMATCH
+                    | ServiceRegistrar.TRANSITION_NOMATCH_MATCH
+                    | ServiceRegistrar.TRANSITION_MATCH_MATCH,
+                    listen.proxy, null, Lease.ANY);
+            elease = (Lease) leasePreparer.prepareProxy(reg.getLease());
+            leaseMgr.renewUntil(elease, Lease.ANY, lnotify);
+            eventSource = reg.getSource();
+            eventID = reg.getID();
+            seqNo = reg.getSequenceNumber();
+        } catch (Throwable t) {
+            failure(t);
+        }
+    }
+
+    private void failure(Throwable t) {
+        logger.log(Level.INFO, "call to lookup service failed", t);
+        ((DiscoveryManagement) disco).discard(lookup);
+    }
+
+    class ServiceItemRenderer implements ListCellRenderer {
+
+        private JLabel label;
+
+        public ServiceItemRenderer() {
+            label = new JLabel();
+            label.setOpaque(true);
+        }
+
+        public Component getListCellRendererComponent(JList list,
+                Object value,
+                int index,
+                boolean isSelected,
+                boolean cellHasFocus) {
+            ServiceListItem item = null;
+            if (value instanceof ServiceListItem) {
+                item = (ServiceListItem) value;
+            }
+
+            label.setFont(list.getFont());
+
+            if (isSelected) {
+                label.setBackground(list.getSelectionBackground());
+                label.setForeground(list.getSelectionForeground());
+            } else {
+                label.setBackground(list.getBackground());
+                label.setForeground(list.getForeground());
+            }
+            if (item != null) {
+                // accessible check done in this method
+                label.setIcon(item.getIcon());
+                label.setText(item.getTitle());
+            } else {
+                label.setText(value.toString());
+            }
+
+            return label;
+        }
+    }
+    private static Icon[] icons = new Icon[3];
+
+    static {
+        // Administrable Service, Controllable Attribute
+        icons[0] = MetalIcons.getBlueFolderIcon();
+        // Non-administrable Service
+        icons[1] = MetalIcons.getGrayFolderIcon();
+        // "Connection Refused" Service
+        icons[2] = MetalIcons.getUnusableFolderIcon();
+    }
+
+    private class ServiceListItem {
+
+        private ServiceItem item;
+        private boolean isAccessible;
+        private Object admin = null;
+
+        public ServiceListItem(ServiceItem item) {
+            this.item = item;
+            isAccessible = (item.service != null);
+        }
+
+        public String getTitle() {
+            if (item.service == null) {
+                return "Unknown service";
+            }
+
+            HashSet set = new HashSet();
+            Class[] infs = getInterfaces(item.service.getClass());
+            for (int j = 0; j < infs.length; j++) {
+                set.add(infs[j].getName());
+            }
+
+            // remove known interfaces
+            set.removeAll(ignoreInterfaces);
+
+            String title = null;
+            if (set.size() == 1) {
+                Iterator iter = set.iterator();
+                title = (String) iter.next();
+            } else {
+                title = item.service.getClass().getName();
+                title += " [";
+                for (Iterator iter = set.iterator(); iter.hasNext();) {
+                    title += (String) iter.next();
+                    if (iter.hasNext()) {
+                        title += ", ";
+                    }
+                }
+                title += "]";
+            }
+            if (!isAccessible) {
+                title += " (Stale service)";
+            }
+            return title;
+        }
+
+        public boolean isAccessible() {
+            getAdmin();
+            return isAccessible;
+        }
+
+        public Object getAdmin() {
+            if (admin == null
+                    && isAccessible
+                    && item.service instanceof Administrable) {
+                try {
+                    admin = adminPreparer.prepareProxy(
+                            ((Administrable) item.service).getAdmin());
+                } catch (Throwable t) {
+                    logger.log(Levels.HANDLED, "failed to get admin proxy", t);
+                    isAccessible = false;
+                }
+            }
+            return admin;
+        }
+
+        public boolean isAdministrable() {
+            getAdmin();
+            return (admin instanceof DestroyAdmin
+                    || admin instanceof JoinAdmin
+                    || admin instanceof DiscoveryAdmin);
+        }
+
+        public boolean isSpaceBrowsable() {
+            return false;
+        }
+
+        private boolean isUI() {
+            Entry[] attrs = item.attributeSets;
+            if ((attrs != null) && (attrs.length != 0)) {
+                for (int i = 0; i < attrs.length; i++) {
+                    if (attrs[i] instanceof UIDescriptor) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        public ServiceItem getServiceItem() {
+            return item;
+        }
+
+        public Entry[] getAttributes() {
+            return item.attributeSets;
+        }
+
+        public Icon getIcon() {
+            if (!isAccessible()) {
+                return icons[2];
+            } else if (isAdministrable()) {
+                return icons[0];
+            } else {
+                return icons[1];
+            }
+        }
+
+        public String toString() {
+            return isAccessible()
+                    ? item.service.getClass().getName() : "Unknown service";
+        }
+    }
+
+    private class MouseReceiver extends MouseAdapter {
+
+        private ServiceListPopup popup;
+
+        public MouseReceiver(ServiceListPopup popup) {
+            this.popup = popup;
+        }
+
+        public void mouseClicked(MouseEvent ev) {
+            if (ev.getClickCount() >= 2) {
+                ServiceListItem listItem = getTargetListItem(ev);
+                if (listItem != null) {
+                    ServiceItem item = listItem.getServiceItem();
+                    if (listItem.isAdministrable()) {
+                        new ServiceEditor(item, listItem.getAdmin(), lookup,
+                                Browser.this).setVisible(true);
+                    } else if (listItem.isAccessible()) {
+                        new ServiceBrowser(item, lookup,
+                                Browser.this).setVisible(true);
+                    }
+                }
+            }
+        }
+
+        public void mouseReleased(MouseEvent ev) {
+            if (ev.isPopupTrigger() && (getTargetListItem(ev) != null)) {
+                popup.setServiceItem(getTargetListItem(ev));
+                popup.show(ev.getComponent(), ev.getX(), ev.getY());
+            }
+        }
+
+        public void mousePressed(MouseEvent ev) {
+            if (ev.isPopupTrigger() && (getTargetListItem(ev) != null)) {
+                popup.setServiceItem(getTargetListItem(ev));
+                popup.show(ev.getComponent(), ev.getX(), ev.getY());
+            }
+        }
+
+        private ServiceListItem getTargetListItem(MouseEvent ev) {
+            int index = list.locationToIndex(ev.getPoint());
+            if (index >= 0) {
+                return (ServiceListItem) listModel.getElementAt(index);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private class ServiceListPopup
+            extends JPopupMenu
+            implements ActionListener, PopupMenuListener {
+
+        protected JMenuItem infoItem;
+        protected JMenuItem browseItem;
+        protected JMenuItem adminItem;
+        protected JMenuItem spaceItem;
+        protected ServiceListItem listItem;
+        protected JMenuItem uiItem;
+        protected ServiceItem item;
+
+        public ServiceListPopup() {
+            super();
+
+            ActionListener me = wrap(this);
+            infoItem = new JMenuItem("Show Info");
+            infoItem.addActionListener(me);
+            infoItem.setActionCommand("showInfo");
+            add(infoItem);
+
+            browseItem = new JMenuItem("Browse Service");
+            browseItem.addActionListener(me);
+            browseItem.setActionCommand("browseService");
+            add(browseItem);
+
+            adminItem = new JMenuItem("Admin Service");
+            adminItem.addActionListener(me);
+            adminItem.setActionCommand("adminService");
+            add(adminItem);
+
+            spaceItem = new JMenuItem("Browse Entries");
+            spaceItem.addActionListener(me);
+            spaceItem.setActionCommand("browseEntry");
+            add(spaceItem);
+
+            uiItem = new JMenuItem("Show UI");
+            uiItem.addActionListener(me);
+            uiItem.setActionCommand("showUI");
+            add(uiItem);
+
+            addPopupMenuListener(this);
+            setOpaque(true);
+            setLightWeightPopupEnabled(true);
+        }
+
+        public void setServiceItem(ServiceListItem listItem) {
+            this.listItem = listItem;
+            item = listItem.getServiceItem();
+            infoItem.setEnabled(listItem.isAccessible());
+            browseItem.setEnabled(listItem.isAccessible());
+            adminItem.setEnabled(listItem.isAdministrable());
+            spaceItem.setEnabled(listItem.isSpaceBrowsable());
+            uiItem.setEnabled(listItem.isUI());
+        }
+
+        public void actionPerformed(ActionEvent ev) {
+            String command = ev.getActionCommand();
+
+            if (command.equals("showInfo")) {
+                Class[] infs = getInterfaces(item.service.getClass());
+                String[] msg = new String[3 + infs.length];
+                msg[0] = "ServiceID: " + item.serviceID;
+                msg[1] = ("Service Instance: "
+                        + item.service.getClass().getName());
+                if (infs.length == 1) {
+                    msg[2] = "Implemented Interface:";
+                } else {
+                    msg[2] = "Implemented Interfaces:";
+                }
+                for (int i = 0; i < infs.length; i++) {
+                    msg[3 + i] = infs[i].getName();
+                }
+
+                JOptionPane.showMessageDialog(Browser.this,
+                        msg,
+                        "ServiceItem Information",
+                        JOptionPane.INFORMATION_MESSAGE);
+            } else if (command.equals("browseService")) {
+                new ServiceBrowser(item, lookup,
+                        Browser.this).setVisible(true);
+            } else if (command.equals("adminService")) {
+                new ServiceEditor(item, listItem.getAdmin(),
+                        lookup, Browser.this).setVisible(true);
+            } else if (command.equals("browseEntry")) {
+                // Not supported at the present time.
+            } else if (command.equals("showUI")) {
+                UIDescriptor uiDescriptor = getSelectedUIDescriptor();
+
+                if (uiDescriptor == null) {
+                    return;
+                }
+
+                try {
+                    JFrameFactory uiFactory = (JFrameFactory) uiDescriptor.getUIFactory(
+                            Thread.currentThread().getContextClassLoader());
+                    JFrame frame = uiFactory.getJFrame(item);
+
+                    frame.validate();
+                    frame.setVisible(true);
+                } catch (Exception e) {
+                    logger.log(Level.INFO, "show ui failed", e);
+                    JOptionPane.showMessageDialog(Browser.this,
+                            e.getMessage(),
+                            e.getClass().getName(),
+                            JOptionPane.WARNING_MESSAGE);
+                    return;
+                }
+            }
+        }
+
+        public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
+        }
+
+        public void popupMenuWillBecomeInvisible(PopupMenuEvent ev) {
+        }
+
+        public void popupMenuCanceled(PopupMenuEvent ev) {
+        }
+
+        private UIDescriptor getSelectedUIDescriptor() {
+
+            if (!(new ServiceListItem(item)).isUI()) {
+                return null;
+            }
+
+            Entry[] attrs = item.attributeSets;
+            if ((attrs != null) && (attrs.length != 0)) {
+                for (int i = 0; i < attrs.length; i++) {
+                    if (attrs[i] instanceof UIDescriptor) {
+                        UIDescriptor desc = (UIDescriptor) attrs[i];
+                        if (!"javax.swing".equals(desc.toolkit)) {
+                            continue;
+                        }
+                        return desc;
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
+    private class Exiter extends WindowAdapter {
+
+        public void windowClosing(WindowEvent e) {
+            Browser.this.exiter.actionPerformed(
+                    new ActionEvent(e.getSource(), e.getID(),
+                    "Exit", System.currentTimeMillis(), 0));
+        }
+    }
+
+    /**
+     * An action listener that cancels any lookup service event registration
+     * lease and then calls {@link System#exit System.exit}.
+     */
+    public static class Exit implements ActionListener {
+
+        /**
+         * Cancels any lookup service event registration lease and calls {@link System#exit System.exit}
+         * <code>(0)</code>.
+         */
+        public void actionPerformed(ActionEvent ev) {
+            Object src = ev.getSource();
+            while (!(src instanceof Browser) && src instanceof Component) {
+                if (src instanceof JPopupMenu) {
+                    src = ((JPopupMenu) src).getInvoker();
+                } else {
+                    src = ((Component) src).getParent();
+                }
+            }
+            if (src instanceof Browser) {
+                try {
+                    ((Browser) src).cancelLease();
+                } catch (Throwable t) {
+                    t.printStackTrace();
+                }
+            }
+            System.exit(0);
+        }
+    }
+
+    /**
+     * Runs the service browser. See the package documentation for details.
+     *
+     * @param args command line arguments
+     */
+    public static void main(String[] args) {
+        if (System.getSecurityManager() == null) {
+            System.setSecurityManager(new SecurityManager());
+        }
+        try {
+            final Configuration config =
+                    ConfigurationProvider.getInstance(
+                    args, Browser.class.getClassLoader());
+            LoginContext login =
+                    (LoginContext) config.getEntry(BROWSER, "loginContext",
+                    LoginContext.class, null);
+            if (login != null) {
+                login.login();
+            }
+            PrivilegedExceptionAction action =
+                    new PrivilegedExceptionAction() {
+                public Object run() throws Exception {
+                    return new Browser(null, config);
+                }
+            };
+            if (login != null) {
+                Subject.doAsPrivileged(login.getSubject(), action, null);
+            } else {
+                action.run();
+            }
+        } catch (Throwable t) {
+            if (t instanceof PrivilegedActionException) {
+                t = t.getCause();
+            }
+            logger.log(Level.SEVERE, "browser initialization failed", t);
+        }
+    }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/EntryTreePanel.java b/browser/src/main/java/org/apache/river/container/examples/browser/EntryTreePanel.java
new file mode 100644
index 0000000..95ff9f6
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/EntryTreePanel.java
@@ -0,0 +1,292 @@
+/*
+ * 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.river.container.examples.browser;
+
+import net.jini.core.entry.Entry;
+import java.lang.reflect.Field;
+import java.lang.reflect.Array;
+import java.awt.Component;
+import java.awt.BorderLayout;
+import java.util.logging.Level;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTree;
+import javax.swing.JLabel;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeCellRenderer;
+import javax.swing.plaf.metal.MetalLookAndFeel;
+
+/**
+ *
+ * @author Sun Microsystems, Inc.
+ *
+ * @version 0.2 06/04/98
+ *
+ */
+abstract class EntryTreePanel extends JPanel {
+  /**
+   * running mode. 
+   *
+   * @serial
+   */
+  private boolean isControllable;
+
+
+  /**
+   * @serial
+   */
+
+  private JScrollPane scrollPane;
+
+  /**
+   * @serial
+   */
+  protected JTree tree;
+
+  /**
+   * @serial
+   */
+  protected ObjectNode root;
+
+  /**
+   * @serial
+   */
+  protected DefaultTreeModel model;
+
+  /**
+   * @serial
+   */
+  private boolean showModifier = false;
+
+  /**
+   * @serial
+   */
+  private boolean showPackage = false;
+
+
+  public EntryTreePanel(boolean isControllable) {
+    this.isControllable = isControllable;
+
+    // Init this panel
+    setLayout(new BorderLayout());
+
+    // Init tree node and model (attribute tree nodes)
+    root = new ObjectNode(isControllable);
+    //initTree();
+    model = new DefaultTreeModel(root);
+
+    // Init tree view
+    tree = new JTree(model);
+    //tree.addMouseListener(new DoubleClicker(this));
+    tree.setRootVisible(false);
+    ObjectNodeRenderer renderer = new ObjectNodeRenderer();
+    tree.setCellRenderer(renderer);
+    tree.setRowHeight(0);	// let the renderer handle it
+    scrollPane = new JScrollPane(tree);
+    add(scrollPane, "Center");
+
+    tree.validate();
+    scrollPane.validate();
+  }
+
+  protected abstract Entry[] getEntryArray();
+
+
+  protected void initTree() {
+    Entry[] entries = getEntryArray();
+    if(entries == null)
+      entries = new Entry[0];
+
+    for(int i = 0; i < entries.length; i++){
+      // check controllability
+      boolean nodeControllable = false;
+      if(isControllable && ! (entries[i] instanceof net.jini.lookup.entry.ServiceControlled)) {
+	nodeControllable = true;
+      }
+
+      ObjectNode node = new ObjectNode(entries[i], nodeControllable);
+      root.add(node);
+      try {
+	recursiveObjectTree(node);
+      } catch(IllegalAccessException e){
+	Browser.logger.log(Level.INFO, "entry access failed", e);
+      }
+    }
+  }
+
+  public void refreshPanel() {
+    // reconstruct nodes
+    root.removeAllChildren();
+    initTree();
+    
+    model.nodeStructureChanged(root);
+
+    tree.validate();
+    scrollPane.validate();
+  }
+
+  protected void recursiveObjectTree(ObjectNode node)
+    throws IllegalArgumentException, IllegalAccessException {
+
+    Object obj = node.getObject();
+    if(obj == null)
+      return;
+    //Field[] fields = obj.getClass().getDeclaredFields();
+    Field[] fields = obj.getClass().getFields();
+
+    for(int i = 0; i < fields.length; i++){
+      Field f = fields[i];
+
+      if(Introspector.isHidden(f))
+	continue;
+
+      Class clazz = f.getType();
+      ObjectNode child = null;
+      String fname = f.getName();
+      if(clazz.isPrimitive()){
+	String clazzName = clazz.toString();
+	Object fobj = null;
+	if("int".equals(clazzName)){
+	  fobj = new Integer(f.getInt(obj));
+	} else if("boolean".equals(clazzName)){
+	  fobj = new Boolean(f.getBoolean(obj));
+	} else if("byte".equals(clazzName)){
+	  fobj = new Byte(f.getByte(obj));
+	} else if("char".equals(clazzName)){
+	  fobj = new Character(f.getChar(obj));
+	} else if("double".equals(clazzName)){
+	  fobj = new Double(f.getDouble(obj));
+	} else if("float".equals(clazzName)){
+	  fobj = new Float(f.getFloat(obj));
+	} else if("long".equals(clazzName)){
+	  fobj = new Long(f.getLong(obj));
+	}
+
+	child = new ObjectNode(fobj, clazz, fname, true);
+      } else if(Introspector.isWrapper(clazz) || Introspector.isString(clazz)) {
+	child = new ObjectNode(f.get(obj), clazz, fname, true);
+      } else if(clazz.isArray()){
+	child = new ObjectNode(f.get(obj), clazz, fname, false);
+	child.setAdministrable(node.isAdministrable());
+	child.setControllable(node.isControllable());
+	recursiveArrayTree(child, f);
+      } else {
+	// unknown type
+	Object subobj = f.get(obj);
+
+	// check if sub object has a viewable members.
+	if(countViewableFields(clazz) > 0){
+	  child = new ObjectNode(subobj, clazz, fname, false);
+	  child.setAdministrable(node.isAdministrable());
+	  child.setControllable(node.isControllable());
+	  recursiveObjectTree(child);
+	} else {
+	  child = new ObjectNode(subobj, clazz, fname, true);
+	}
+      }
+      node.add(child);
+    }
+  }
+
+  private int countViewableFields(Class clazz) {
+
+    int count = 0;
+    //Field[] fields = obj.getClass().getDeclaredFields();
+    //Field[] fields = obj.getClass().getFields();
+    Field[] fields = clazz.getFields();
+    for(int i = 0; i < fields.length; i++){
+      Field f = fields[i];
+
+      if(Introspector.isHidden(f))
+	continue;
+
+      count++;
+    }
+
+    return count;
+  }
+
+  private void recursiveArrayTree(ObjectNode node, Field f)
+    throws IllegalArgumentException, IllegalAccessException {
+
+    String name = node.getFieldName();
+    Object aobj = node.getObject();
+
+    int length = Array.getLength(aobj);
+
+    Class clazz = f.getType().getComponentType();
+
+    if(clazz.isPrimitive() || Introspector.isWrapper(clazz) || Introspector.isString(clazz)){
+      // primitive, wrapper objects, string array
+      for(int i = 0; i < length; i++){
+	Object elem = Array.get(aobj, i);
+	//String fname = name + "[" + i + "]"; 
+	ObjectNode child = new ObjectNode(elem, clazz, name, i, true);
+	node.add(child);
+      }
+    } else {
+      // Object or Array (*sigh*)
+      for(int i = 0; i < length; i++){
+	Object elem = Array.get(aobj, i);
+	//String fname = name + "[" + i + "]"; 
+	ObjectNode child = new ObjectNode(elem, clazz, name, i, false);
+	recursiveObjectTree(child);
+	child.setAdministrable(node.isAdministrable());
+	child.setControllable(node.isControllable());
+
+	node.add(child);
+      }
+    }
+  }
+
+
+  class ObjectNodeRenderer implements TreeCellRenderer {
+    private JLabel label;
+
+    public ObjectNodeRenderer() {
+      label = new JLabel();
+      label.setOpaque(true);
+    }
+    
+    public Component getTreeCellRendererComponent(JTree tree,
+						  Object value,
+						  boolean isSelected,
+						  boolean isExpanded,
+						  boolean isLeaf,
+						  int row,
+						  boolean cellHasFocus){
+
+      //label.setFont(tree.getFont());
+      label.setForeground(tree.getForeground());
+      if(isSelected){
+	//label.setBackground(UIManager.getColor("Tree.backgroundSelectionColor"));
+	//label.setForeground(UIManager.getColor("Tree.textSelectionColor"));
+	label.setBackground(MetalLookAndFeel.getPrimaryControl());
+      } else {
+	//label.setBackground(UIManager.getColor("Tree.backgroundNonSelectionColor"));
+	//label.setForeground(UIManager.getColor("Tree.textNonSelectionColor"));
+	label.setBackground(tree.getBackground());
+      }
+
+      ObjectNode node = (ObjectNode) value;
+      label.setText(node.getTitle());
+      label.setIcon(node.getIcon());
+      return label;
+    }
+  }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/Introspector.java b/browser/src/main/java/org/apache/river/container/examples/browser/Introspector.java
new file mode 100644
index 0000000..e7781d5
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/Introspector.java
@@ -0,0 +1,106 @@
+/*
+ * 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.river.container.examples.browser;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Array;
+import java.lang.reflect.Modifier;
+
+/**
+ * Introspection related methods
+ *
+ * @author Sun Microsystems, Inc.
+ *
+ * @version 0.2 06/04/98
+ *
+ */
+
+class Introspector {
+
+  public static boolean isHidden(Field f){
+    int m = f.getModifiers();
+
+    if(Modifier.isPrivate(m) || Modifier.isStatic(m))
+      return true;
+
+    return false;
+  }
+
+  public static boolean isString(Class clazz){
+    if("java.lang.String".equals(clazz.getName()))
+      return true;
+    return false;
+  }
+
+  public static boolean isWrapper(Class clazz){
+    String name = clazz.getName();
+
+    if("java.lang.Integer".equals(name) ||
+       "java.lang.Boolean".equals(name) ||
+       "java.lang.Byte".equals(name) ||
+       "java.lang.Char".equals(name) ||
+       "java.lang.Double".equals(name) ||
+       "java.lang.Float".equals(name) ||
+       "java.lang.Long".equals(name)){
+      return true;
+    }
+    return false;
+  }
+
+  /** Return the name of an interface or primitive type, handling arrays. */
+  public static String getTypename(Class t, boolean showPackage) {
+    String brackets = "";
+    while(t.isArray()) {
+      brackets += "[]";
+      t = t.getComponentType();
+    }
+
+    if(showPackage)
+      return t.getName() + brackets;
+    else
+      return extractClassName(t.getName()) + brackets;
+  }
+
+  /** Return a string version of modifiers, handling spaces nicely. */
+  public static String getModifierString(int m) {
+    if(m == 0)
+      return "";
+    else
+      return Modifier.toString(m) + " ";
+  }
+
+  /** Print the modifiers, type, and name of a field. */
+  public static String getFieldString(Field f, boolean showModifier, boolean showPackage) {
+    String fstring = "";
+    
+    if(showModifier)
+      fstring += getModifierString(f.getModifiers());
+
+    fstring += getTypename(f.getType(), showPackage);
+    fstring += " ";
+    fstring += f.getName();
+
+    return fstring;
+  }
+
+  public static String extractClassName(String fullName){
+    int index = fullName.lastIndexOf(".");
+    
+    return fullName.substring(index + 1);
+  }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/MetalIcons.java b/browser/src/main/java/org/apache/river/container/examples/browser/MetalIcons.java
new file mode 100644
index 0000000..30987bb
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/MetalIcons.java
@@ -0,0 +1,348 @@
+/*
+ * 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.river.container.examples.browser;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import javax.swing.plaf.metal.MetalLookAndFeel;
+import java.io.Serializable;
+import javax.swing.Icon;
+
+/**
+ * Based on "MetalIconFactory.java"
+ *
+ * @author Sun Microsystems, Inc.
+ *
+ */
+class MetalIcons implements Serializable {
+
+  private static Icon blueFolderIcon;
+  private static Icon blueFileIcon;
+  private static Icon orangeFolderIcon;
+  private static Icon orangeFileIcon;
+  private static Icon grayFolderIcon;
+  private static Icon grayFileIcon;
+  private static Icon unusableFolderIcon;
+  private static Icon unusableFileIcon;
+
+  // Colors for Orange icons
+  private static Color orangeDarkShadowColor = new Color(0xcc, 0x99, 0x66);
+  private static Color orangeColor           = new Color(0xff, 0x99, 0x33);
+  private static Color orangeShadowColor     = new Color(0xcc, 0x99, 0x66);
+  private static Color orangeInfoColor       = new Color(0x99, 0x00, 0x33);
+  private static Color orangeHighlightColor  = new Color(0xff, 0xff, 0x33);
+
+  // Colors for Gray icons
+  private static Color grayDarkShadowColor = new Color(0x99, 0x99, 0x99);
+  private static Color grayColor           = new Color(0xbb, 0xbb, 0xbb);
+  private static Color grayShadowColor     = new Color(0xaa, 0xaa, 0xaa);
+  private static Color grayInfoColor       = new Color(0x88, 0x88, 0x88);
+  private static Color grayHighlightColor  = new Color(0xdd, 0xdd, 0xdd);
+
+  // Constants
+  public static final boolean DARK = false;
+  public static final boolean LIGHT = true;
+
+  // Accessor functions for Icons. Does the caching work.
+  public static Icon getBlueFolderIcon() {
+    if(blueFolderIcon == null)
+      blueFolderIcon = new BlueFolderIcon();
+    return blueFolderIcon;
+  }
+
+  public static Icon getBlueFileIcon() {
+    if(blueFileIcon == null)
+      blueFileIcon = new BlueFileIcon();
+    return blueFileIcon;
+  }
+
+  public static Icon getOrangeFolderIcon() {
+    if(orangeFolderIcon == null)
+      orangeFolderIcon = new OrangeFolderIcon();
+    return orangeFolderIcon;
+  }
+
+  public static Icon getOrangeFileIcon() {
+    if(orangeFileIcon == null)
+      orangeFileIcon = new OrangeFileIcon();
+    return orangeFileIcon;
+  }
+
+  public static Icon getGrayFolderIcon() {
+    if(grayFolderIcon == null)
+      grayFolderIcon = new GrayFolderIcon();
+    return grayFolderIcon;
+  }
+
+  public static Icon getGrayFileIcon() {
+    if(grayFileIcon == null)
+      grayFileIcon = new GrayFileIcon();
+    return grayFileIcon;
+  }
+
+  public static Icon getUnusableFolderIcon() {
+    if(unusableFolderIcon == null)
+      unusableFolderIcon = new UnusableFolderIcon();
+    return unusableFolderIcon;
+  }
+
+  public static Icon getUnusableFileIcon() {
+    if(unusableFileIcon == null)
+      unusableFileIcon = new UnusableFileIcon();
+    return unusableFileIcon;
+  }
+
+
+  static private final Dimension folderIcon16Size = new Dimension( 16, 16 );
+  static private final Dimension fileIcon16Size = new Dimension( 16, 16 );
+
+  /**
+   */
+  static class BlueFolderIcon extends FolderIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      MetalLookAndFeel.getPrimaryControlDarkShadow(),
+		      MetalLookAndFeel.getPrimaryControl(),
+		      MetalLookAndFeel.getPrimaryControlShadow(),
+		      MetalLookAndFeel.getPrimaryControlInfo(),
+		      MetalLookAndFeel.getPrimaryControlHighlight());
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class OrangeFolderIcon extends FolderIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      orangeDarkShadowColor,
+		      orangeColor,
+		      orangeShadowColor,
+		      orangeInfoColor,
+		      orangeHighlightColor);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class GrayFolderIcon extends FolderIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      grayDarkShadowColor,
+		      grayColor,
+		      grayShadowColor,
+		      grayInfoColor,
+		      grayHighlightColor);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class UnusableFolderIcon extends FolderIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      grayDarkShadowColor,
+		      grayColor,
+		      grayShadowColor,
+		      grayInfoColor,
+		      grayHighlightColor);
+      super.drawCross(c, g, x, y);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  /**
+   */
+  private static abstract class FolderIcon16 implements Serializable {
+
+    public void paintIcon(Component c, Graphics g, int x, int y,
+			  Color controlDarkShadowColor,
+			  Color controlColor,
+			  Color controlShadowColor,
+			  Color controlInfoColor,
+			  Color controlHighlightColor) {
+      g.translate( x, y + getShift() );
+
+      int right = folderIcon16Size.width - 1;
+      int bottom = folderIcon16Size.height - 1;
+
+      // Draw tab top
+      g.setColor( controlDarkShadowColor );
+      g.drawLine( right - 5, 3, right, 3 );
+      g.drawLine( right - 6, 4, right, 4 );
+
+      // Draw folder front
+      g.setColor( controlColor );
+      g.fillRect( 2, 7, 13, 8 );
+
+      // Draw tab bottom
+      g.setColor( controlShadowColor );
+      g.drawLine( right - 6, 5, right - 1, 5 );
+
+      // Draw outline
+      g.setColor( controlInfoColor );
+      g.drawLine( 0, 6, 0, bottom );            // left side
+      g.drawLine( 1, 5, right - 7, 5 );         // first part of top
+      g.drawLine( right - 6, 6, right - 1, 6 ); // second part of top
+      g.drawLine( right, 5, right, bottom );    // right side
+      g.drawLine( 0, bottom, right, bottom );   // bottom
+
+      // Draw highlight
+      g.setColor( controlHighlightColor );
+      g.drawLine( 1, 6, 1, bottom - 1 );
+      g.drawLine( 1, 6, right - 7, 6 );
+      g.drawLine( right - 6, 7, right - 1, 7 );
+
+      g.translate( -x, -(y + getShift()) );
+    }
+
+    public void drawCross(Component c, Graphics g, int x, int y) {
+      g.translate( x, y + getShift() );
+
+      int right = folderIcon16Size.width - 1;
+      int bottom = folderIcon16Size.height - 1;
+
+      // Draw tab top
+      Color crossColor = Color.red;
+      g.setColor( crossColor );
+      g.drawLine( 2, 2, right, bottom );
+      g.drawLine( 2, bottom, right, 2 );
+    }
+
+    public int getShift() { return 0; }
+    public int getAdditionalHeight() { return 0; }
+
+    public int getIconWidth() { return folderIcon16Size.width; }
+    public int getIconHeight() { return folderIcon16Size.height + getAdditionalHeight(); }
+  }
+
+
+
+
+  static class BlueFileIcon extends FileIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      MetalLookAndFeel.getPrimaryControlHighlight(),
+		      MetalLookAndFeel.getPrimaryControlInfo(),
+		      MetalLookAndFeel.getPrimaryControl());
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class OrangeFileIcon extends FileIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      orangeHighlightColor,
+		      orangeInfoColor,
+		      orangeColor);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class GrayFileIcon extends FileIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      grayHighlightColor,
+		      grayInfoColor,
+		      grayColor);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  static class UnusableFileIcon extends FileIcon16 implements Icon {
+    public void paintIcon(Component c, Graphics g, int x, int y) {
+      super.paintIcon(c, g, x, y,
+		      grayHighlightColor,
+		      grayInfoColor,
+		      grayColor);
+      super.drawCross(c, g, x, y);
+    }
+    public int getShift() { return -1; }
+    public int getAdditionalHeight() { return 2; }
+  }
+
+  /**
+   */
+  private static abstract class FileIcon16 implements Serializable {
+    public void paintIcon(Component c, Graphics g, int x, int y,
+			  Color controlHighlightColor,
+			  Color controlInfoColor,
+			  Color controlColor) {
+      g.translate( x, y + getShift() );
+
+      int right = fileIcon16Size.width - 1;
+      int bottom = fileIcon16Size.height - 1;
+
+      // Draw fill
+      g.setColor( controlHighlightColor );
+      g.fillRect( 4, 2, 9, 12 );
+
+      // Draw frame
+      g.setColor( controlInfoColor );
+      g.drawLine( 2, 0, 2, bottom );                 // left
+      g.drawLine( 2, 0, right - 4, 0 );              // top
+      g.drawLine( 2, bottom, right - 1, bottom );    // bottom
+      g.drawLine( right - 1, 6, right - 1, bottom ); // right
+      g.drawLine( right - 6, 2, right - 2, 6 );      // slant 1
+      g.drawLine( right - 5, 1, right - 4, 1 );      // part of slant 2
+      g.drawLine( right - 3, 2, right - 3, 3 );      // part of slant 2
+      g.drawLine( right - 2, 4, right - 2, 5 );      // part of slant 2
+
+      // Draw highlight
+      g.setColor( controlColor );
+      g.drawLine( 3, 1, 3, bottom - 1 );                  // left
+      g.drawLine( 3, 1, right - 6, 1 );                   // top
+      g.drawLine( right - 2, 7, right - 2, bottom - 1 );  // right
+      g.drawLine( right - 5, 2, right - 3, 4 );           // slant
+      g.drawLine( 3, bottom - 1, right - 2, bottom - 1 ); // bottom
+
+      g.translate( -x, -(y + getShift()) );
+    }
+
+    public void drawCross(Component c, Graphics g, int x, int y) {
+      g.translate( x, y + getShift() );
+
+      int right = folderIcon16Size.width - 1;
+      int bottom = folderIcon16Size.height - 1;
+
+      // Draw tab top
+      Color crossColor = Color.red;
+      g.setColor( crossColor );
+      g.drawLine( 1, 1, right - 1, bottom - 1 );
+      g.drawLine( 1, bottom - 1, right - 1, 1 );
+    }
+
+    public int getShift() { return 0; }
+    public int getAdditionalHeight() { return 0; }
+
+    public int getIconWidth() { return fileIcon16Size.width; }
+    public int getIconHeight() { return fileIcon16Size.height + getAdditionalHeight(); }
+  }
+
+
+  static class TreeLeafIcon extends FileIcon16 {
+    public int getShift() { return 2; }
+    public int getAdditionalHeight() { return 4; }
+  }
+}
+
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/ObjectNode.java b/browser/src/main/java/org/apache/river/container/examples/browser/ObjectNode.java
new file mode 100644
index 0000000..6409ba1
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/ObjectNode.java
@@ -0,0 +1,316 @@
+/*
+ * 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.river.container.examples.browser;
+
+import java.lang.reflect.Field;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.Icon;
+
+/**
+ *
+ * @author Sun Microsystems, Inc.
+ *
+ * @version 0.2 06/04/98
+ *
+ */
+class ObjectNode extends DefaultMutableTreeNode implements java.io.Serializable {
+
+  // "classname", "classname fieldName" or "classname fieldName=value"
+  /**
+   * @serial
+   */
+  private String name;
+
+  /**
+   * @serial
+   */
+  private Object obj;
+
+  /**
+   * @serial
+   */
+  private Class clazz;
+
+  /**
+   * @serial
+   */
+  private String fieldName;
+
+  /**
+   * @serial
+   */
+  private int arrayIndex = -1;
+
+  /**
+   * @serial
+   */
+  private boolean editable = false;
+
+  /**
+   * @serial
+   */
+  private boolean isLeaf;
+
+  /**
+   * @serial
+   */
+  private boolean isAdministrable;	// root level
+
+  /**
+   * @serial
+   */
+  private boolean isControllable;	// entry level
+
+  /**
+   * @serial
+   */
+  private boolean isRoot = false;
+
+  /**
+   * @serial
+   */
+  private boolean isEntryTop = false;
+
+  // icons
+  private static Icon[] icons = new Icon[6];
+  static {
+    icons[0] = MetalIcons.getBlueFolderIcon();	// Administrable Service, Controllable Attribute
+    icons[1] = MetalIcons.getGrayFolderIcon();	// Non-administrable Service
+    icons[2] = MetalIcons.getOrangeFolderIcon();	// Uncontrollable Attribute
+    icons[3] = MetalIcons.getBlueFileIcon();	// Administrable Service, Controllable Attribute
+    icons[4] = MetalIcons.getGrayFileIcon();	// Non-administrable Service
+    icons[5] = MetalIcons.getOrangeFileIcon();	// Uncontrollable Attribute
+  }
+
+  /**
+     Constructor for a root node.
+  */
+  public ObjectNode(boolean isAdministrable) {
+    this("Root node", "".getClass(), null, -1, false);
+
+    this.isAdministrable = isAdministrable;
+    this.isRoot = true;
+    this.isEntryTop = true;
+  }
+
+  /**
+     Constructor for an entry (attribute) top nodes.
+  */
+  public ObjectNode(Object obj, boolean isControllable) {
+    this(obj, obj.getClass(), null, -1, false);
+
+    this.isControllable = isControllable;
+    this.isEntryTop = true;
+  }
+
+  /**
+     Constructor for an ordinary field.
+  */
+  public ObjectNode(Object obj, Class clazz, String fieldName, boolean isLeaf) {
+    this(obj, clazz, fieldName, -1, isLeaf);
+  }
+
+  /**
+     Constructor for an array element.
+  */
+  public ObjectNode(Object obj, Class clazz, String fieldName, int arrayIndex, boolean isLeaf) {
+    this.obj = obj;
+    this.clazz = clazz;
+    this.fieldName = fieldName;
+    this.arrayIndex = arrayIndex;
+    this.isLeaf = isLeaf;
+
+    super.setAllowsChildren(! isLeaf);
+    setNodeName();
+  }
+
+  private void setNodeName() {
+    name = Introspector.getTypename(clazz, false);
+    if(fieldName != null)
+      name += " " + fieldName;
+
+    if(isLeaf) {
+      //Class clazz = obj.getClass();
+      String value = "";
+      if(arrayIndex >= 0)
+	value += ("[" + arrayIndex + "]");
+      value += "=";
+      if(clazz.isPrimitive()){
+	value += "" + obj;
+	editable = false;
+      } else if(Introspector.isWrapper(clazz)){
+	// Wrapper objects
+	value += "" + obj;
+	editable = true;
+      } else if(Introspector.isString(clazz)) {
+	value += "\"" + obj + "\"";
+	editable = true;
+      } else {
+	value += (obj == null ? "null" : obj.toString());
+      }
+      name += value;
+    } else if(obj == null) {
+      name += "=null";
+    }
+
+    super.setUserObject(name);
+  }
+
+  public void add(ObjectNode child){
+    child.setAdministrable(isAdministrable);
+    if(! isRoot){
+      child.setControllable(isControllable);
+    }
+
+    super.add(child);
+  }
+
+  public Object getEntryTop() {
+    ObjectNode snode = this;
+    do {
+      snode = (ObjectNode) snode.getParent();
+    } while(! snode.isEntryTop());
+
+    return snode.getObject();
+  }
+
+  protected boolean isEntryTop() {
+    return isEntryTop;
+  }
+
+  public void setObjectRecursive() throws NoSuchFieldException, IllegalAccessException {
+    ObjectNode pnode = this;
+    do {
+      pnode = (ObjectNode) pnode.getParent();
+      Object pobj = pnode.getObject();
+      // Needs to think about array modifications
+      Field f = pobj.getClass().getField(fieldName);
+      f.set(pobj, obj);
+    } while(! pnode.isEntryTop());
+  }
+
+  public String getTitle() {
+    return name;
+  }
+
+  public Icon getIcon() {
+    /*
+      if(isAdministrable){
+      if(isControllable){
+      if(isLeaf)	    return icons[3];
+      else		    return icons[0];
+      } else {
+      if(isLeaf)	    return icons[5];
+      else		    return icons[2];
+      }
+      } else {
+      if(isLeaf)	    return icons[4];
+      else		    return icons[1];
+      }
+    */
+    if(isAdministrable && isControllable)
+      if(isLeaf)	return icons[3];
+      else		return icons[0];
+    else
+      if(isLeaf)	return icons[4];
+      else		return icons[1];
+  }
+
+
+  // Overwrite
+  public void setUserObject(Object obj){
+    if(obj instanceof String)
+      name = (String) obj;
+
+    super.setUserObject(obj);
+  }
+
+  public Object getUserObject(){
+    return name;
+  }
+
+  public String toString() {
+    return name;
+  }
+
+  public String getFieldName() {
+    return fieldName;
+  }
+
+  public Object getObject() {
+    return obj;
+  }
+
+  public boolean isEditable() {
+    return editable;
+  }
+
+  public boolean isAdministrable() {
+    return isAdministrable;
+  }
+
+  public void setAdministrable(boolean val) {
+    isAdministrable = val;
+  }
+
+  public boolean isControllable() {
+    return isControllable;
+  }
+
+  public void setControllable(boolean val) {
+    isControllable = val;
+  }
+
+  public Object setValue(Object val) throws NumberFormatException {
+    String clazzName = clazz.getName();
+    Object newObj = null;
+
+    if(val instanceof String || val == null) {
+      String sval = (String) val;
+      if(clazzName.equals("java.lang.Integer"))
+	newObj = new Integer(sval);
+      else if(clazzName.equals("java.lang.Boolean"))
+	newObj = new Boolean(sval);
+      else if(clazzName.equals("java.lang.Byte"))
+	newObj = new Byte(sval);
+      else if(clazzName.equals("java.lang.Character"))
+	newObj = new Character(sval.charAt(0));
+      else if(clazzName.equals("java.lang.Double"))
+	newObj = new Double(sval);
+      else if(clazzName.equals("java.lang.Float"))
+	newObj = new Float(sval);
+      else if(clazzName.equals("java.lang.Long"))
+	newObj = new Long(sval);
+      else if(clazzName.equals("java.lang.String"))
+	newObj = new String(sval);	// clone 
+    } else if(val.getClass().equals(obj.getClass())) {
+      // same class type
+      newObj = val;
+    }
+
+    Object oldObj = obj;
+    obj = newObj;
+
+    setNodeName();
+
+    return oldObj;
+  }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/ServiceBrowser.java b/browser/src/main/java/org/apache/river/container/examples/browser/ServiceBrowser.java
new file mode 100644
index 0000000..6af743b
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/ServiceBrowser.java
@@ -0,0 +1,282 @@
+/*
+ * 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.river.container.examples.browser;
+
+import net.jini.core.lookup.ServiceItem;
+import net.jini.core.lookup.ServiceRegistrar;
+import net.jini.core.lookup.ServiceMatches;
+import net.jini.core.lookup.ServiceTemplate;
+import net.jini.core.entry.Entry;
+import net.jini.lookup.ui.factory.JFrameFactory;
+import net.jini.lookup.entry.UIDescriptor;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.logging.Level;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.event.PopupMenuListener;
+import javax.swing.event.PopupMenuEvent;
+
+/**
+ * A browser utility to browse entries in a specified space.
+ *
+ * @author Sun Microsystems, Inc.
+ *
+ * @version 0.2 06/04/98
+ *
+ */
+class ServiceBrowser extends JFrame {
+  private Browser browser;
+  private AttributePanel attrPanel;
+  private final static int MINIMUM_WINDOW_WIDTH = 320;
+
+  public ServiceBrowser(ServiceItem item,
+			ServiceRegistrar registrar,
+			Browser browser)
+  {
+    super("ServiceItem Browser");
+
+    this.browser = browser;
+    // init main components
+    attrPanel = new AttributePanel(item, registrar);
+
+    // add menu and attr panel
+    getContentPane().setLayout(new BorderLayout());
+    getContentPane().add(new BrowserMenuBar(), "North");
+    getContentPane().add(attrPanel, "Center");
+
+    validate();
+    pack();
+    setSize(((getSize().width < MINIMUM_WINDOW_WIDTH) ? MINIMUM_WINDOW_WIDTH : getSize().width),
+	    getSize().height);
+
+    // center in parent frame
+    Rectangle bounds = browser.getBounds();
+    Dimension dialogSize = getPreferredSize();
+    int xpos = bounds.x + (bounds.width - dialogSize.width)/ 2;
+    int ypos = bounds.y + (bounds.height - dialogSize.height)/2;
+    setLocation((xpos < 0) ? 0 : xpos,
+		(ypos < 0) ? 0 : ypos);
+  }
+
+
+  class BrowserMenuBar extends JMenuBar {
+    public BrowserMenuBar() {
+      JMenuItem mitem;
+
+      // "File" Menu
+      JMenu fileMenu = (JMenu) add(new JMenu("File"));
+      mitem = (JMenuItem) fileMenu.add(new JMenuItem("Refresh"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  attrPanel.refreshPanel();
+	}
+      }));
+      mitem = (JMenuItem) fileMenu.add(new JMenuItem("Close"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  ServiceBrowser.this.setVisible(false);
+	}
+      }));
+    }
+  }
+
+  class AttributePanel extends EntryTreePanel {
+    private ServiceItem item;
+    private ServiceRegistrar registrar;
+
+    public AttributePanel(ServiceItem item, ServiceRegistrar registrar) {
+      super(false);	// Entries are not editable.
+
+      this.item = item;
+      this.registrar = registrar;
+
+      tree.addMouseListener(browser.wrap(new ServiceBrowser.MouseReceiver(item,
+					 uiDescriptorPopup())));
+
+      refreshPanel();
+    }
+
+    protected Entry[] getEntryArray() {
+      try{
+	ServiceMatches matches = registrar.lookup(new ServiceTemplate(item.serviceID,
+								      new Class[] { item.service.getClass() },
+								      new Entry[] {}),
+						  10);
+	if(matches.totalMatches != 1)
+	  Browser.logger.log(Level.INFO, "unexpected lookup matches: {0}",
+			     new Integer(matches.totalMatches));
+	else
+	  return matches.items[0].attributeSets;
+      } catch (Throwable t) {
+	Browser.logger.log(Level.INFO, "lookup failed", t);
+      }
+      return null;
+    }
+  }
+
+    // provides support for ServiceUI
+    public class UIDescriptorPopup extends JPopupMenu implements ActionListener,
+	PopupMenuListener {
+
+	protected transient JMenuItem showUIItem;
+	protected transient ServiceItem serviceItem;
+
+	public UIDescriptorPopup() {
+	    super();
+
+	    showUIItem = new JMenuItem("Show UI");
+
+	    showUIItem.addActionListener(this);
+	    showUIItem.setActionCommand("showUI");
+	    add(showUIItem);
+
+	    addPopupMenuListener(this);
+	    setOpaque(true);
+	    setLightWeightPopupEnabled(true);
+	}
+
+	public void actionPerformed(ActionEvent anEvent) {
+
+	    UIDescriptor uiDescriptor = getSelectedUIDescriptor();
+
+	    if (uiDescriptor == null) {
+		return;
+	    }
+
+	    try {
+		JFrameFactory uiFactory = (JFrameFactory)
+		    uiDescriptor.getUIFactory(Thread.currentThread().getContextClassLoader());
+		JFrame frame = uiFactory.getJFrame(serviceItem);
+
+		frame.validate();
+		frame.setVisible(true);
+	    }
+	    catch (Exception e) {
+		e.printStackTrace();
+
+		return;
+	    }
+	}
+
+	public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
+	}
+
+	public void popupMenuWillBecomeInvisible(PopupMenuEvent ev) {
+	}
+
+	public void popupMenuCanceled(PopupMenuEvent ev) {
+	}
+
+	public void setServiceItem(ServiceItem anItem) {
+	    serviceItem = anItem;
+	}
+    }
+
+    class MouseReceiver extends MouseAdapter {
+
+	private ServiceBrowser.UIDescriptorPopup popup;
+	private ServiceItem serviceItem;
+
+	public MouseReceiver(ServiceItem aServiceItem,
+			ServiceBrowser.UIDescriptorPopup popup) {
+		this.popup = popup;
+		serviceItem = aServiceItem;
+	}
+
+	public void mouseReleased(MouseEvent ev) {
+
+	    higlightSelection(ev);
+
+	    if (!ev.isPopupTrigger()) {
+		return;
+	    }
+
+	    UIDescriptor selectedDescriptor = getSelectedUIDescriptor();
+
+	    if (selectedDescriptor == null) {
+		return;
+	    }
+
+	    if (!"javax.swing".equals(selectedDescriptor.toolkit)) {
+		return;
+	    }
+
+	    popup.setServiceItem(serviceItem);
+	    popup.show(ev.getComponent(), ev.getX(), ev.getY());
+	}
+
+	public void mousePressed(MouseEvent ev) {
+
+	    higlightSelection(ev);
+
+	    if (!ev.isPopupTrigger()) {
+		return;
+	    }
+
+	    UIDescriptor selectedDescriptor = getSelectedUIDescriptor();
+
+	    if (selectedDescriptor == null) {
+		return;
+	    }
+
+	    if (!"javax.swing".equals(selectedDescriptor.toolkit)) {
+		return;
+	    }
+
+	    popup.setServiceItem(serviceItem);
+	    popup.show(ev.getComponent(), ev.getX(), ev.getY());
+	}
+    }
+
+    private UIDescriptor getSelectedUIDescriptor() {
+
+	ObjectNode selectedNode =
+		(ObjectNode) attrPanel.tree.getLastSelectedPathComponent();
+
+	if (selectedNode == null) {
+	    return null;
+	}
+
+	Object selectedObject = selectedNode.getObject();
+
+	try {
+	    return (UIDescriptor) selectedObject;
+	}
+	catch (ClassCastException e) {
+	    return null;
+	}
+    }
+
+    private void higlightSelection(MouseEvent anEvent) {
+	attrPanel.tree.setSelectionPath(attrPanel.tree.getPathForLocation(
+	    anEvent.getX(), anEvent.getY()));
+    }
+
+    private ServiceBrowser.UIDescriptorPopup uiDescriptorPopup() {
+	return new ServiceBrowser.UIDescriptorPopup();
+    }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/ServiceEditor.java b/browser/src/main/java/org/apache/river/container/examples/browser/ServiceEditor.java
new file mode 100644
index 0000000..ee972da
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/ServiceEditor.java
@@ -0,0 +1,1213 @@
+/*
+ * 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.river.container.examples.browser;
+
+import com.sun.jini.admin.DestroyAdmin;
+import com.sun.jini.config.Config;
+import com.sun.jini.logging.Levels;
+import com.sun.jini.proxy.BasicProxyTrustVerifier;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.rmi.server.ExportException;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JList;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JTree;
+import javax.swing.SwingUtilities;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+import javax.swing.tree.TreePath;
+import net.jini.admin.JoinAdmin;
+import net.jini.config.ConfigurationException;
+import net.jini.core.discovery.LookupLocator;
+import net.jini.core.entry.Entry;
+import net.jini.core.event.EventRegistration;
+import net.jini.core.event.RemoteEvent;
+import net.jini.core.event.RemoteEventListener;
+import net.jini.core.lease.Lease;
+import net.jini.core.lookup.ServiceEvent;
+import net.jini.core.lookup.ServiceItem;
+import net.jini.core.lookup.ServiceMatches;
+import net.jini.core.lookup.ServiceRegistrar;
+import net.jini.core.lookup.ServiceTemplate;
+import net.jini.export.Exporter;
+import net.jini.jeri.BasicILFactory;
+import net.jini.jeri.BasicJeriExporter;
+import net.jini.jeri.tcp.TcpServerEndpoint;
+import net.jini.lookup.DiscoveryAdmin;
+import net.jini.lookup.entry.UIDescriptor;
+import net.jini.lookup.ui.factory.JFrameFactory;
+import net.jini.security.TrustVerifier;
+import net.jini.security.proxytrust.ServerProxyTrust;
+
+/**
+ * ServiceEditor is a gui-based utility to add/modify
+ * attributes, groups and lookup locators.
+ * And also supports some well known admin interfaces.
+ * (DestroyAdmin, DiscoveryAdmin)
+ *
+ * <p>
+ * current issues<br>
+ * <ul>
+ *   <li> can't operate(add/remove/modify) array elements in an entry.
+ *   <li> does not support EntryBean.
+ *   <li> field modification is not based on EditableTree
+ * </ul>
+ *
+ * @author Sun Microsystems, Inc.
+ */
+class ServiceEditor extends JFrame {
+  private static final Logger logger = Browser.logger;
+
+  private Browser browser;
+  private ServiceItem item;
+  private ServiceRegistrar registrar;
+  protected Object admin;
+  private ServiceTemplate stmpl;
+  private NotifyReceiver receiver;
+  private Lease elease = null;
+  private long eventID = 0;
+  private long seqNo = Long.MAX_VALUE;
+  private AttributeTreePanel attrPanel;
+
+  private final static int MINIMUM_WINDOW_WIDTH = 320;
+
+  public ServiceEditor(ServiceItem item,
+		       Object admin,
+		       ServiceRegistrar registrar,
+		       Browser browser)
+    {
+    super("ServiceItem Editor");
+    
+    this.item = item;
+    this.admin = admin;
+    this.registrar = registrar;
+    this.browser = browser;
+
+    // init main components
+    attrPanel = new AttributeTreePanel();
+
+    // setup notify
+    try {
+      stmpl = new ServiceTemplate(item.serviceID,
+				  new Class[] { item.service.getClass() },
+				  new Entry[] {});
+      receiver = new NotifyReceiver();
+
+      setupNotify();
+    } catch (Throwable t) {
+      logger.log(Level.INFO, "event registration failed", t);
+      cancelNotify();
+    }
+
+    addWindowListener(browser.wrap(new WindowAdapter() {
+	public void windowClosing(WindowEvent e) {
+	  cleanup();
+	}
+      }));
+    // add menu and attr panel
+    getContentPane().setLayout(new BorderLayout());
+    getContentPane().add(new JoinMenuBar(), "North");
+    getContentPane().add(attrPanel, "Center");
+
+    validate();
+    pack();
+    setSize(((getSize().width < MINIMUM_WINDOW_WIDTH) ? MINIMUM_WINDOW_WIDTH : getSize().width),
+	    getSize().height);
+
+    // center in parent frame
+    Rectangle bounds = browser.getBounds();
+    Dimension dialogSize = getPreferredSize();
+    int xpos = bounds.x + (bounds.width - dialogSize.width)/ 2;
+    int ypos = bounds.y + (bounds.height - dialogSize.height)/2;
+    setLocation((xpos < 0) ? 0 : xpos,
+		(ypos < 0) ? 0 : ypos);
+  }
+
+  void cleanup() {
+    // cancel lease
+    cancelNotify();
+    // release resources and close all child frames
+    dispose();
+    receiver.unexport();
+  }
+
+  protected void cancelNotify() {
+    if(elease != null) {
+      try {
+	browser.leaseMgr.cancel(elease);
+      } catch (Throwable t) {
+	logger.log(Levels.HANDLED, "event cancellation failed", t);
+      }
+      elease = null;
+      seqNo = Long.MAX_VALUE;
+    }
+  }
+
+  protected void setupNotify() {
+    if(registrar != null) {
+      try {
+	EventRegistration reg =
+	    registrar.notify(stmpl,
+			     ServiceRegistrar.TRANSITION_MATCH_NOMATCH |
+			     ServiceRegistrar.TRANSITION_NOMATCH_MATCH |
+			     ServiceRegistrar.TRANSITION_MATCH_MATCH,
+			     receiver.proxy,
+			     null,
+			     Lease.ANY);
+	elease = (Lease) browser.leasePreparer.prepareProxy(reg.getLease());
+	browser.leaseMgr.renewUntil(elease, Lease.ANY,
+				    new Browser.LeaseNotify());
+	eventID = reg.getID();
+	seqNo = reg.getSequenceNumber();
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "event registration failed", t);
+      }
+    }
+  }
+
+  private class NotifyReceiver implements RemoteEventListener, ServerProxyTrust
+  {
+    private final Exporter exporter;
+    final RemoteEventListener proxy;
+
+    public NotifyReceiver() throws ConfigurationException, ExportException {
+      exporter = (Exporter)
+	  Config.getNonNullEntry(browser.config, Browser.BROWSER,
+				 "listenerExporter", Exporter.class,
+				 new BasicJeriExporter(
+					     TcpServerEndpoint.getInstance(0),
+					     new BasicILFactory(),
+					     false, false));
+      proxy = (RemoteEventListener) exporter.export(this);
+    }
+
+    public void notify(final RemoteEvent ev) {
+      SwingUtilities.invokeLater(browser.wrap(new Runnable() {
+	public void run() {
+	  if (eventID == ev.getID() && seqNo < ev.getSequenceNumber()) {
+	    seqNo = ev.getSequenceNumber();
+	    attrPanel.receiveNotify(((ServiceEvent) ev).getTransition());
+	  }
+	}
+      }));
+    }
+
+    public TrustVerifier getProxyVerifier() {
+      return new BasicProxyTrustVerifier(proxy);
+    }
+
+    void unexport() {
+	exporter.unexport(true);
+    }
+  }
+
+  class JoinMenuBar extends JMenuBar {
+    public JoinMenuBar() {
+      JMenuItem mitem;
+
+      // "File" Menu
+      JMenu fileMenu = (JMenu) add(new JMenu("File"));
+      mitem = (JMenuItem) fileMenu.add(new JMenuItem("Show Info"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  Class[] infs = Browser.getInterfaces(item.service.getClass());
+	  String[] msg = new String[3 + infs.length];
+	  msg[0] = "ServiceID: " + item.serviceID;
+	  msg[1] = "Service Instance: " + item.service.getClass().getName();
+	  if(infs.length == 1)
+	    msg[2] = "Implemented Interface:";
+	  else
+	    msg[2] = "Implemented Interfaces:";
+	  for(int i = 0; i < infs.length; i++)
+	    msg[3 + i] = "    " + infs[i].getName();
+
+	  JOptionPane.showMessageDialog(ServiceEditor.this,
+					msg,
+					"ServiceItem Information",
+					JOptionPane.INFORMATION_MESSAGE);
+	}
+      }));
+      mitem = (JMenuItem) fileMenu.add(new JMenuItem("Refresh"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  attrPanel.refreshPanel();
+	}
+      }));
+      mitem = (JMenuItem) fileMenu.add(new JMenuItem("Close"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  cleanup();
+	}
+      }));
+
+      // "Edit" Menu
+      JMenu editMenu = (JMenu) add(new JMenu("Edit"));
+      mitem = (JMenuItem) editMenu.add(new JMenuItem("Add Attribute..."));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  attrPanel.addAttr();
+	}
+      }));
+      if(! (admin instanceof JoinAdmin))
+	mitem.setEnabled(false);
+      mitem = (JMenuItem) editMenu.add(new JMenuItem("Remove Attribute"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  attrPanel.removeAttr();
+	}
+      }));
+      if(! (admin instanceof JoinAdmin))
+	mitem.setEnabled(false);
+
+      // "Admin" Menu
+      JMenu adminMenu = (JMenu) add(new JMenu("Admin"));
+
+      // Group (JoinAdmin)
+      mitem = (JMenuItem) adminMenu.add(new JMenuItem("Joining groups..."));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  new GroupLister("Joining Groups").showFrame();
+	}
+      }));
+      if(! (admin instanceof JoinAdmin))
+	mitem.setEnabled(false);
+
+      // Locator (JoinAdmin)
+      mitem = (JMenuItem) adminMenu.add(new JMenuItem("Joining locators..."));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  new LocatorLister("Joining Locators").showFrame();
+	}
+      }));
+      if(! (admin instanceof JoinAdmin))
+	mitem.setEnabled(false);
+
+      // separator
+      adminMenu.addSeparator();
+
+      // Group (DiscoveryAdmin)
+      mitem = (JMenuItem) adminMenu.add(new JMenuItem("Member groups..."));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  new MemberGroupLister("Member Groups").showFrame();
+	}
+      }));
+      if(! (admin instanceof DiscoveryAdmin))
+	mitem.setEnabled(false);
+
+      // Unicast port (DiscoveryAdmin)
+      mitem = (JMenuItem) adminMenu.add(new JMenuItem("Unicast port..."));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  try {
+	    String[] msg = { "Current port is " + ((DiscoveryAdmin) admin).getUnicastPort(),
+			     "Input a new value" };
+	    String result = JOptionPane.showInputDialog(ServiceEditor.this,
+							msg,
+							"Unicast Port",
+							JOptionPane.QUESTION_MESSAGE);
+
+	    if(result == null)
+	      return;
+
+	    try {
+	      int port = Integer.parseInt(result);
+	      ((DiscoveryAdmin) admin).setUnicastPort(port);
+	    } catch (NumberFormatException e) {
+	      JOptionPane.showMessageDialog(ServiceEditor.this,
+					    result + " is not acceptable.",
+					    "Error",
+					    JOptionPane.ERROR_MESSAGE);
+	    } catch (Throwable t) {
+	      logger.log(Level.INFO, "setting unicast port failed", t);
+	      JOptionPane.showMessageDialog(ServiceEditor.this,
+					    t.getMessage(),
+					    t.getClass().getName(),
+					    JOptionPane.ERROR_MESSAGE);
+	    }
+	  } catch (Throwable t) {
+	    logger.log(Level.INFO, "getting unicast port failed", t);
+	    JOptionPane.showMessageDialog(ServiceEditor.this,
+					  t.getMessage(),
+					  t.getClass().getName(),
+					  JOptionPane.ERROR_MESSAGE);
+	  }
+	}
+      }));
+      if(! (admin instanceof DiscoveryAdmin))
+	mitem.setEnabled(false);
+
+      // separator
+      adminMenu.addSeparator();
+
+      // DestroyAdmin
+      mitem = (JMenuItem) adminMenu.add(new JMenuItem("Destroy"));
+      mitem.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  if(JOptionPane.showConfirmDialog(ServiceEditor.this,
+					   "Are you sure to destroy this service?",
+					   "Query",
+					   JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
+	    try {
+	      ((DestroyAdmin) admin).destroy();
+	      cleanup();
+	    } catch (Throwable t) {
+	      logger.log(Level.INFO, "service destroy failed", t);
+	      JOptionPane.showMessageDialog(ServiceEditor.this,
+					    t.getMessage(),
+					    t.getClass().getName(),
+					    JOptionPane.ERROR_MESSAGE);
+	    }
+	}
+      }));
+      if(! (admin instanceof DestroyAdmin))
+	mitem.setEnabled(false);
+    }
+  }
+
+  class AttributeTreePanel extends EntryTreePanel {
+
+    public AttributeTreePanel() {
+      super(admin instanceof JoinAdmin);
+
+      if(admin instanceof JoinAdmin) {
+	tree.addMouseListener(browser.wrap(new DoubleClicker(this)));
+      }
+      tree.addMouseListener(browser.wrap(new MouseReceiver(item, uiDescriptorPopup())));
+
+      refreshPanel();
+    }
+
+    protected Entry[] getEntryArray() {
+      if(admin instanceof JoinAdmin) {
+	try {
+	  item.attributeSets = ((JoinAdmin) admin).getLookupAttributes();
+	} catch (Throwable t) {
+	  logger.log(Level.INFO, "obtaining attributes failed", t);
+	}
+      } else {
+	try{
+	  ServiceMatches matches = registrar.lookup(stmpl, 1);
+	  if(matches.totalMatches != 1)
+	    Browser.logger.log(Level.INFO, "unexpected lookup matches: {0}",
+			       new Integer(matches.totalMatches));
+	  else
+	    item.attributeSets = matches.items[0].attributeSets;
+	} catch (Throwable t) {
+	  Browser.logger.log(Level.INFO, "lookup failed", t);
+	}
+      }
+      return item.attributeSets;
+    }
+
+    protected void receiveNotify(int transition) {
+
+      if (browser.isAutoConfirm()) {
+        if (transition == ServiceRegistrar.TRANSITION_MATCH_NOMATCH)
+          cleanup();
+        else
+          refreshPanel();
+
+        return;
+      }
+
+	String[] msg =
+	    (transition == ServiceRegistrar.TRANSITION_MATCH_NOMATCH) ?
+	    new String[]{
+		"Service has been removed from lookup service.",
+		"Do you want to close the service editor window ?"} :
+	    new String[]{
+		"Attributes have been modified by another client or the service itself.",
+		"Do you want to refresh the attributes ?"};
+      int result = JOptionPane.showConfirmDialog(AttributeTreePanel.this,
+						 msg,
+						 "Query",
+						 JOptionPane.YES_NO_OPTION);
+	
+      if(result == JOptionPane.YES_OPTION) {
+	  if (transition == ServiceRegistrar.TRANSITION_MATCH_NOMATCH)
+	      cleanup();
+	  else
+	      refreshPanel();
+      }
+    }
+
+    public void editField(ObjectNode node) {
+
+      String result = JOptionPane.showInputDialog(this,
+						  "Input a new value",
+						  "Modify a field",
+						  JOptionPane.QUESTION_MESSAGE);
+
+      if(result == null){
+      } else {
+	// Save current value as template
+	Entry template = cloneEntry((Entry) node.getEntryTop());
+	Object oldVal = null;
+
+	if(result.length() == 0){
+	  oldVal = node.setValue(null);
+	} else {
+	  oldVal = node.setValue(result);
+	}
+	// modifyAttribute
+	try {
+	  node.setObjectRecursive();
+	  Entry attr = (Entry) node.getEntryTop();
+	  //Entry template = (Entry) generateTemplate(attr);
+
+	  // cancel notify while adding an attribute
+	  cancelNotify();
+
+	  ((JoinAdmin) admin).modifyLookupAttributes(
+			      new Entry[] { template }, new Entry[] { attr });
+
+	  setupNotify();
+
+	  // Redraw node value
+	  model.nodeChanged(node);
+	} catch (Throwable t) {
+	  logger.log(Level.INFO, "attribute modification failed", t);
+	  // recover tree node
+	  try {
+	    node.setValue(oldVal);
+	    node.setObjectRecursive();
+	  } catch (Throwable tt) {
+	    logger.log(Levels.HANDLED, "node reset failed", tt);
+	  }
+	  model.nodeChanged(node);
+	  //model.nodeStructureChanged(node);
+
+	  // show dialog
+	  JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					t.getMessage(),
+					t.getClass().getName(),
+					JOptionPane.ERROR_MESSAGE);
+	}
+      }
+    }
+
+    public void addAttr() {
+
+      String result = JOptionPane.showInputDialog(this,
+						  "Input an entry class name",
+						  "Add an attribute",
+						  JOptionPane.QUESTION_MESSAGE);
+
+      if(result == null || result.length() == 0){
+      } else {
+	try {
+	  Class clazz = Class.forName(result);
+	  Object attr = clazz.newInstance();
+
+	  if(! (attr instanceof Entry)){
+	    JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					  "Does not implement Entry interface",
+					  "Unacceptable Class",
+					  JOptionPane.WARNING_MESSAGE);
+
+	  } else if(attr instanceof net.jini.lookup.entry.ServiceControlled){
+	    JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					  "Implements ServiceControlled interface",
+					  "Unacceptable Class",
+					  JOptionPane.WARNING_MESSAGE);
+	  } else {
+	    // cancel notify while adding an attribute
+	    cancelNotify();
+
+	    ((JoinAdmin) admin).addLookupAttributes(
+						new Entry[] { (Entry) attr });
+	    // add node of this attribute
+	    ObjectNode node = new ObjectNode(attr, true);
+	    root.add(node);
+	    recursiveObjectTree(node);
+
+	    // 
+	    setupNotify();
+
+	    // refresh view
+	    model.nodesWereInserted(root,
+				    new int[] { model.getIndexOfChild(root, node) });
+	  }
+	} catch (ClassNotFoundException e) {
+	  JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					e.getMessage(),
+					"Class Not Found",
+					JOptionPane.WARNING_MESSAGE);
+	} catch (Throwable t) {
+	  logger.log(Level.INFO, "adding attribute failed", t);
+	  JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					t.getMessage(),
+					t.getClass().getName(),
+					JOptionPane.WARNING_MESSAGE);
+	}
+      }
+    }
+
+    public void removeAttr() {
+
+      ObjectNode node = (ObjectNode) tree.getLastSelectedPathComponent();
+      if(node == null){
+	JOptionPane.showMessageDialog(AttributeTreePanel.this,
+				      "Select an attribute folder to remove.",
+				      "Warning",
+				      JOptionPane.WARNING_MESSAGE);
+	
+	return;
+      } else if(! node.isControllable()){
+	JOptionPane.showMessageDialog(AttributeTreePanel.this,
+				      "This attribute is under service provider's control.",
+				      "Warning",
+				      JOptionPane.WARNING_MESSAGE);
+	
+	return;
+      } else if(! node.isEntryTop()){
+	JOptionPane.showMessageDialog(AttributeTreePanel.this,
+				      "Select a top of attribute folder.",
+				      "Warning",
+				      JOptionPane.WARNING_MESSAGE);
+	
+	return;
+      }
+
+      Entry target = (Entry) node.getObject();
+      int result = JOptionPane.showConfirmDialog(AttributeTreePanel.this,
+						 new String[] {"Remove attribute:",
+								 target.toString() },
+						 "Query",
+						 JOptionPane.YES_NO_OPTION);
+	
+      if(result == JOptionPane.YES_OPTION){
+	// Remote Attribute
+	try {
+
+	  // cancel notify while adding an attribute
+	  cancelNotify();
+
+	  ((JoinAdmin) admin).modifyLookupAttributes(
+				new Entry[] { target }, new Entry[] { null });
+
+	  // 
+	  setupNotify();
+
+	  int index = root.getIndex(node);
+	  root.remove(node);
+	  model.nodesWereRemoved(root, new int[] {index}, new Object[] {node});
+	  node = null;
+	} catch (Throwable t) {
+	  logger.log(Level.INFO, "attribute removal failed", t);
+	  JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					t.getMessage(),
+					t.getClass().getName(),
+					JOptionPane.ERROR_MESSAGE);
+	}
+      }
+    }
+
+    private Entry cloneEntry(Entry attr) {
+      try {
+	Class realClass = attr.getClass();
+	Entry template = (Entry) realClass.newInstance();
+
+	Field[] f = realClass.getFields();
+	for(int i = 0; i < f.length; i++) {
+	  if(! usableField(f[i]))
+	    continue;
+	  f[i].set(template, f[i].get(attr));
+	}
+
+	return template;
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "duplicating entry failed", t);
+      }
+      return null;
+    }
+
+    // from EntryRep
+    private boolean usableField(Field field) {
+      Class desc = field.getDeclaringClass();
+
+      if(desc.isPrimitive()) {
+	throw new IllegalArgumentException("primitive types not allowed in an Entry");
+      }
+
+      // skip anything that isn't a public per-object mutable field
+      int mods = field.getModifiers();
+      return (0 == (mods & (Modifier.TRANSIENT | Modifier.STATIC | Modifier.FINAL)));
+    }
+
+    private Entry generateTemplate(Entry attr) {
+      try {
+	Class realClass = attr.getClass();
+	Entry template = (Entry) realClass.newInstance();
+
+	Field[] f = realClass.getFields();
+	for(int i = 0; i < f.length; i++)
+	  f[i].set(template, null);
+
+	return template;
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "instantiating template failed", t);
+      }
+      return null;
+    }
+
+    class DoubleClicker extends MouseAdapter {
+      AttributeTreePanel parent;
+
+      public DoubleClicker(AttributeTreePanel parent){
+	this.parent = parent;
+      }
+
+      public void mouseClicked(MouseEvent ev){
+	if(ev.getClickCount() >= 2){
+	  JTree tree = (JTree) ev.getSource();
+	  TreePath path = tree.getPathForLocation(ev.getX(), ev.getY());
+	  if(path == null)
+	    return;
+	  ObjectNode node = (ObjectNode) path.getLastPathComponent();
+
+	  if(node.isLeaf()){
+	    if(! node.isControllable()){
+	      JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					    "This attribute is under service provider's control.",
+					    "Warning",
+					    JOptionPane.WARNING_MESSAGE);
+	    } else if(node.isEditable() &&
+		      ((ObjectNode) node.getParent()).isEntryTop())
+	    {
+	      parent.editField(node);
+	    } else {
+	      JOptionPane.showMessageDialog(AttributeTreePanel.this,
+					    "This field is not editable.",
+					    "Warning",
+					    JOptionPane.WARNING_MESSAGE);
+	    }
+	  }
+
+	  tree.scrollPathToVisible(path);
+	}
+      }
+    }
+  }
+
+  abstract class ListerFrame extends JFrame {
+
+    private JList listBox;
+    private JScrollPane scrollPane;
+    protected DefaultListModel model = new DefaultListModel();
+    private DefaultListModel dummyModel = new DefaultListModel();	// to keep away from Swing's bug
+
+    private JButton addButton;
+    private JButton removeButton;
+    private JButton closeButton;
+
+    public ListerFrame(String title) {
+      super(title);
+
+      getContentPane().setLayout(new BorderLayout());
+
+      // create the initial list
+      listBox = new JList(model);
+      listBox.setFixedCellHeight(20);
+      scrollPane = new JScrollPane(listBox);
+      getContentPane().add(scrollPane, "Center");
+      //resetListModel();
+
+      // Create the controls 
+      JPanel buttonPanel = new JPanel();
+      addButton = new JButton("Add");
+      addButton.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  String result = JOptionPane.showInputDialog(ListerFrame.this, getAddMessage());
+
+	  if(result != null){
+	    StringTokenizer st = new StringTokenizer(result);
+	    String[] tokens = new String[st.countTokens()];
+	    for(int i = 0; i < tokens.length; i++)
+	      tokens[i] = st.nextToken().trim();
+
+	    addItems(tokens);
+	    resetListModel();
+	    scrollPane.validate();
+	  }
+	}
+      }));
+      buttonPanel.add(addButton);
+
+      removeButton = new JButton("Remove");
+      removeButton.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  Object[] selected = listBox.getSelectedValues();
+
+	  if(selected == null || selected.length == 0){
+	    // no items are selected
+	    JOptionPane.showMessageDialog(ListerFrame.this,
+					  "No items are selected",
+					  "Warning",
+					  JOptionPane.WARNING_MESSAGE);
+	    return;
+	  }
+
+	  int result = JOptionPane.showConfirmDialog(ListerFrame.this,
+						     getRemoveMessage(selected),
+						     "Query",
+						     JOptionPane.YES_NO_OPTION);
+	
+	  if(result == JOptionPane.YES_OPTION){
+	    removeItems(selected);
+	    resetListModel();
+	    scrollPane.validate();
+	  }
+	}
+      }));
+      buttonPanel.add(removeButton);
+
+      closeButton = new JButton("Close");
+      closeButton.addActionListener(browser.wrap(new ActionListener() {
+	public void actionPerformed(ActionEvent ev) {
+	  setVisible(false);
+	}
+      }));
+      buttonPanel.add(closeButton);
+      getContentPane().add(buttonPanel, "South");
+
+      pack();
+    }
+
+    public void showFrame() {
+      // init list data
+      resetListModel();
+
+      // center in parent frame
+      Rectangle bounds = ServiceEditor.this.getBounds();
+      Dimension dialogSize = getPreferredSize();
+
+      setLocation(bounds.x + (bounds.width - dialogSize.width)/ 2,
+		  bounds.y + (bounds.height - dialogSize.height)/2);
+
+      setVisible(true);
+    }
+
+    private void resetListModel() {
+      //listBox.setModel(null);
+      listBox.setModel(dummyModel);	// to keep away from NullException (Swing's bug)
+
+      model.removeAllElements();
+      initListModel();
+
+      listBox.setModel(model);
+      listBox.clearSelection();
+      listBox.ensureIndexIsVisible(0);
+      listBox.repaint();
+      listBox.revalidate();
+    }
+
+    protected abstract void initListModel();
+
+    protected abstract String getAddMessage();
+
+    protected abstract String getRemoveMessage(Object[] items);
+
+    protected abstract void addItems(String[] items);
+
+    protected abstract void removeItems(Object[] items);
+  }
+
+  class GroupLister extends ListerFrame {
+
+    public GroupLister(String title) {
+      super(title);
+    }
+
+    protected void initListModel() {
+	if (!(admin instanceof JoinAdmin)) {
+	    return;
+	}
+
+      try {
+	String[] groups = ((JoinAdmin) admin).getLookupGroups();
+	for(int i = 0; i < groups.length; i++) {
+	  model.addElement(new GroupItem(groups[i]));
+	}
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "obtaining groups failed", t);
+      }
+    }
+
+    protected String getAddMessage() {
+      return "Enter adding group(s)";
+    }
+
+    protected String getRemoveMessage(Object[] items) {
+      StringBuffer msg = new StringBuffer();
+      if(items.length > 1)
+	msg.append("Remove these groups : ");
+      else
+	msg.append("Remove a group : ");
+      for(int i = 0; i < items.length; i++) {
+	if(i != 0)
+	  msg.append(", ");
+	msg.append(((GroupItem) items[i]).toString());
+      }
+      return msg.toString();
+    }
+
+    protected void addItems(String[] items) {
+      // check "public"
+      String[] grps = new String[items.length];
+      for(int i = 0; i < items.length; i++)
+	grps[i] = new GroupItem(items[i]).group;
+
+      try {
+	((JoinAdmin) admin).addLookupGroups(grps);
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "adding groups failed", t);
+      }
+    }
+
+    protected void removeItems(Object[] items) {
+      String[] grps = new String[items.length];
+      for(int i = 0; i < items.length; i++)
+	grps[i] = ((GroupItem) items[i]).group;
+
+      try {
+	((JoinAdmin) admin).removeLookupGroups(grps);
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "removing groups failed", t);
+      }
+    }
+  }
+
+  class MemberGroupLister extends ListerFrame {
+
+    public MemberGroupLister(String title) {
+      super(title);
+    }
+
+    protected void initListModel() {
+      try {
+	String[] groups = ((DiscoveryAdmin) admin).getMemberGroups();
+	for(int i = 0; i < groups.length; i++) {
+	  model.addElement(new GroupItem(groups[i]));
+	}
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "obtaining groups failed", t);
+      }
+    }
+
+    protected String getAddMessage() {
+      return "Enter adding group(s)";
+    }
+
+    protected String getRemoveMessage(Object[] items) {
+      StringBuffer msg = new StringBuffer();
+      if(items.length > 1)
+	msg.append("Remove these groups : ");
+      else
+	msg.append("Remove a group : ");
+      for(int i = 0; i < items.length; i++) {
+	if(i != 0)
+	  msg.append(", ");
+	msg.append(((GroupItem) items[i]).toString());
+      }
+      return msg.toString();
+    }
+
+    protected void addItems(String[] items) {
+      // check "public"
+      String[] grps = new String[items.length];
+      for(int i = 0; i < items.length; i++)
+	grps[i] = new GroupItem(items[i]).group;
+
+      try {
+	((DiscoveryAdmin) admin).addMemberGroups(grps);
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "adding groups failed", t);
+      }
+    }
+
+    protected void removeItems(Object[] items) {
+      String[] grps = new String[items.length];
+      for(int i = 0; i < items.length; i++)
+	grps[i] = ((GroupItem) items[i]).group;
+
+      try {
+	((DiscoveryAdmin) admin).removeMemberGroups(grps);
+      } catch (Throwable t){
+	logger.log(Level.INFO, "removing groups failed", t);
+      }
+    }
+  }
+
+  class GroupItem {
+    public String group;
+
+    public GroupItem(String group) {
+      if(group.equals("public"))
+	this.group = "";
+      else
+	this.group = group;
+    }
+
+    public String toString() {
+      if("".equals(group))
+	return "public";
+      else
+	return group;
+    }
+  }
+
+  class LocatorLister extends ListerFrame {
+
+    public LocatorLister(String title) {
+      super(title);
+    }
+
+    protected void initListModel() {
+	if (!(admin instanceof JoinAdmin)) {
+	    return;
+	}
+
+      try {
+	LookupLocator[] locators = ((JoinAdmin) admin).getLookupLocators();
+	for(int i = 0; i < locators.length; i++) {
+	  model.addElement(locators[i]);
+	}
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "obtaining locators failed", t);
+      }
+    }
+
+    protected String getAddMessage() {
+      return "Enter a new locator's URL";
+    }
+
+    protected String getRemoveMessage(Object[] items) {
+      StringBuffer msg = new StringBuffer();
+      if(items.length > 1)
+	msg.append("Remove these locators : ");
+      else
+	msg.append("Remove a locator : ");
+      for(int i = 0; i < items.length; i++) {
+	if(i != 0)
+	  msg.append(", ");
+	msg.append(items[i].toString());
+      }
+      return msg.toString();
+    }
+
+    protected void addItems(String[] items) {
+      LookupLocator[] locs = new LookupLocator[items.length];
+      for(int i = 0; i < items.length; i++) {
+	try {
+	  locs[i] = new LookupLocator(items[i]);
+	} catch (MalformedURLException e) {
+	  JOptionPane.showMessageDialog(LocatorLister.this,
+					"\"" + items[i] + "\": " +
+					e.getMessage(),
+					"Bad Locator",
+					JOptionPane.WARNING_MESSAGE);
+	  return;
+	}
+      }
+
+      try {
+	((JoinAdmin) admin).addLookupLocators(locs);
+      } catch (Throwable t){
+	logger.log(Level.INFO, "adding locators failed", t);
+      }
+    }
+
+    protected void removeItems(Object[] items) {
+      LookupLocator[] locs = new LookupLocator[items.length];
+      for(int i = 0; i < items.length; i++)
+	locs[i] = (LookupLocator) items[i];
+
+      try {
+	((JoinAdmin) admin).removeLookupLocators(locs);
+      } catch (Throwable t) {
+	logger.log(Level.INFO, "removing locators failed", t);
+      }
+    }
+  }
+
+    // provides support for ServiceUI
+    public class UIDescriptorPopup extends JPopupMenu implements ActionListener,
+	PopupMenuListener {
+
+	protected transient JMenuItem showUIItem;
+	protected transient ServiceItem serviceItem;
+
+	public UIDescriptorPopup() {
+	    super();
+
+	    showUIItem = new JMenuItem("Show UI");
+
+	    showUIItem.addActionListener(this);
+	    showUIItem.setActionCommand("showUI");
+	    add(showUIItem);
+
+	    addPopupMenuListener(this);
+	    setOpaque(true);
+	    setLightWeightPopupEnabled(true);
+	}
+
+	public void actionPerformed(ActionEvent anEvent) {
+
+	    UIDescriptor uiDescriptor = getSelectedUIDescriptor();
+
+	    if (uiDescriptor == null) {
+		return;
+	    }
+
+	    try {
+		JFrameFactory uiFactory = (JFrameFactory)
+		    uiDescriptor.getUIFactory(Thread.currentThread().getContextClassLoader());
+		JFrame frame = uiFactory.getJFrame(serviceItem);
+
+		frame.validate();
+		frame.setVisible(true);
+	    }
+	    catch (Exception e) {
+		e.printStackTrace();
+
+		return;
+	    }
+	}
+
+	public void popupMenuWillBecomeVisible(PopupMenuEvent ev) {
+	}
+
+	public void popupMenuWillBecomeInvisible(PopupMenuEvent ev) {
+	}
+
+	public void popupMenuCanceled(PopupMenuEvent ev) {
+	}
+
+	public void setServiceItem(ServiceItem anItem) {
+	    serviceItem = anItem;
+	}
+    }
+
+    class MouseReceiver extends MouseAdapter {
+
+	private ServiceEditor.UIDescriptorPopup popup;
+	private ServiceItem serviceItem;
+
+	public MouseReceiver(ServiceItem aServiceItem,
+			ServiceEditor.UIDescriptorPopup popup) {
+		this.popup = popup;
+		serviceItem = aServiceItem;
+	}
+
+	public void mouseReleased(MouseEvent ev) {
+
+	    higlightSelection(ev);
+
+	    if (!ev.isPopupTrigger()) {
+		return;
+	    }
+
+	    UIDescriptor selectedDescriptor = getSelectedUIDescriptor();
+
+	    if (selectedDescriptor == null) {
+		return;
+	    }
+
+	    if (!"javax.swing".equals(selectedDescriptor.toolkit)) {
+		return;
+	    }
+
+	    popup.setServiceItem(serviceItem);
+	    popup.show(ev.getComponent(), ev.getX(), ev.getY());
+	}
+
+	public void mousePressed(MouseEvent ev) {
+
+	    higlightSelection(ev);
+
+	    if (!ev.isPopupTrigger()) {
+		return;
+	    }
+
+	    UIDescriptor selectedDescriptor = getSelectedUIDescriptor();
+
+	    if (selectedDescriptor == null) {
+		return;
+	    }
+
+	    if (!"javax.swing".equals(selectedDescriptor.toolkit)) {
+		return;
+	    }
+
+	    popup.setServiceItem(serviceItem);
+	    popup.show(ev.getComponent(), ev.getX(), ev.getY());
+	}
+    }
+
+    private UIDescriptor getSelectedUIDescriptor() {
+
+	ObjectNode selectedNode =
+		(ObjectNode) attrPanel.tree.getLastSelectedPathComponent();
+
+	if (selectedNode == null) {
+	    return null;
+	}
+
+	Object selectedObject = selectedNode.getObject();
+
+	try {
+	    return (UIDescriptor) selectedObject;
+	}
+	catch (ClassCastException e) {
+	    return null;
+	}
+    }
+
+    private void higlightSelection(MouseEvent event) {
+	attrPanel.tree.setSelectionPath(attrPanel.tree.getPathForLocation(
+	    event.getX(), event.getY()));
+    }
+
+    private ServiceEditor.UIDescriptorPopup uiDescriptorPopup() {
+	return new ServiceEditor.UIDescriptorPopup();
+    }
+}
diff --git a/browser/src/main/java/org/apache/river/container/examples/browser/package.html b/browser/src/main/java/org/apache/river/container/examples/browser/package.html
new file mode 100644
index 0000000..50eb836
--- /dev/null
+++ b/browser/src/main/java/org/apache/river/container/examples/browser/package.html
@@ -0,0 +1,787 @@
+<!--
+ ! 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.
+ !-->
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
+        "http://www.w3.org/TR/1998/REC-html40-19980424/strict.dtd">
+<html>
+<head>
+<title>Service Browser</title>
+</head>
+<body text="#000000" bgcolor="#ffffff" link="#9b37cc"
+      vlink="#cc1877" alink="#ffffff">
+A visualization tool for exploring Jini Network Technology
+communities (<i>djinns</i>) with support for ServiceUI. The Service Browser
+lets you discover lookup services and inspect the various
+services registered within those lookup services. This tool
+is provided as an example program within the Apache River release
+but many who use it think
+of it more as a utility than an example.
+<p>
+This document provides the following information about the Service Browser:
+<ul>
+<li><a href="#running">Running the Service Browser</a>
+<li><a href="#discovering">Discovering and Selecting a Lookup Service</a>
+<li><a href="#inspecting">Inspecting Services</a>
+<li><a href="#filtering">Filtering Services</a>
+<li><a href="#miscops">Other Operations</a>
+<li><a href="#configuring">Configuring the Service Browser</a>
+<li><a href="#logging">Loggers and Logging Levels</a>
+<li><a href="#examples">Examples for Running the Service Browser</a>
+</ul>
+
+<a name="running"></a>
+<h2>Running the Service Browser</h2>
+The Service Browser is started with the following command line 
+structure:
+<blockquote><pre>
+% java <var><b>java_options</b></var>
+       -jar <var><b>install_dir</b></var>/lib/browser.jar
+       <var><b>config_options</b></var>
+</pre></blockquote>
+where <var><b>java_options</b></var> includes such things as the security
+policy file and the RMI codebase annotation, <var><b>install_dir</b></var>
+is the directory where the Apache River release is installed, and
+<var><b>config_options</b></var> includes any
+{@link net.jini.config.Configuration} provider options. The
+Service Browser can be run with no <var><b>config_options</b></var>.
+For the RMI codebase annotation,
+the system property <code>java.rmi.server.codebase</code> should be set to
+two URLs that provide HTTP-based access to the Service Browser's download
+JAR files,
+<code><var><b>install_dir</b></var>/lib-dl/browser-dl.jar</code> and
+<code><var><b>install_dir</b></var>/lib-dl/jsk-dl.jar</code>, in that order.
+<p>
+The Service Browser also can be run as a nonactivatable entity under the
+{@linkplain com.sun.jini.start Service Starter}, using
+<code>com.sun.jini.example.browser.Browser</code> for the implementation
+class, <code><var><b>install_dir</b></var>/lib/browser.jar</code> for the
+implementation class path, and a codebase containing URLs for
+<code><var><b>install_dir</b></var>/lib-dl/browser-dl.jar</code> and
+<code><var><b>install_dir</b></var>/lib-dl/jsk-dl.jar</code> (in that order).
+
+<a name="discovering"></a>
+<h2>Discovering and Selecting a Lookup Service</h2>
+The lookup services that will be discovered are initially controlled
+by configuration entries, with the default being to perform multicast
+discovery for all lookup services. If you specify one or more groups or
+addresses in a configuration file (as described in
+<a href="#configuring">Configuring the Service Browser</a>),
+the browser only discovers lookup services that are members of those groups
+or that are at those addresses.
+<p>
+The Service Browser presents a single window when it is started. The
+window has the menu items <b>File</b>, <b>Registrar</b>, <b>Options</b>,
+<b>Services</b>, and <b>Attributes</b>. The upper text area changes to
+show which groups and/or addresses are being used for discovery and
+how many lookup services have been discovered.
+The picture below shows a typical Service Browser main window, in which
+nine lookup services have been discovered, but none have yet been selected.
+<p>
+<img src="doc-files/browser1.gif" 
+Alt="Picture of the lookup browser as it looks when it first starts.">
+<p>
+As lookup services are discovered,
+they appear under the <b>Registrar</b> menu, listed by unicast address.
+A specific lookup service can then be selected from this menu for browsing.
+When a lookup service is selected, the upper text area will be revised to
+show the number of services registered in that lookup service, and the lower
+folder area will show the registered services. Administrable services
+are shown with a blue icon, and non-administrable services are shown with
+a gray icon. As new services register or existing services unregister,
+the contents of the window will change automatically to reflect them.
+<p>
+After initial start up, different lookup services can be discovered
+using several items available from the <b>File</b> menu:
+<ul>
+    <li><b>Find All</b> will perform multicast discovery for all lookup
+	services. This is the default behavior of the
+	browser when it first starts.</li>
+    <li><b>Find Public</b> will perform multicast discovery for all lookup
+	services that are members of the <code>public</code> group.</li>
+    <li><b>Find By Group...</b> will present a dialog box allowing you to
+         specify one or more group names (separated by commas or whitespace).
+	 Multicast discovery will then be performed to find all lookup
+	 services that are members of those groups.</li>
+    <li><b>Find By Address...</b> will present a dialog box allowing you to
+	 specify one or more unicast addresses (separated by commas or
+	 whitespace). Unicast discovery will then be performed to find the
+	 lookup services at those adddresses.</li>
+</ul>
+<p>
+For example, the picture below shows the dialog box presented when the
+<b>Find By Group...</b> menu item is selected. 
+<p>
+<img src="doc-files/browser2.gif"
+Alt="Picture of the lookup browser with File menu pulled down.">
+<p>
+In this next picture, the group name <code>examples.jini.sun.com</code>
+will be entered.
+<p>
+<img src="doc-files/browser3.gif"
+Alt="Picture of the lookup browser with group dialog box visible.">
+<p>
+After the group <code>examples.jini.sun.com</code> has been entered, a list of
+discovered lookup services will appear in the <b>Registrar</b> menu.
+In this example, only the host
+<code>kahlua</code> has a lookup service running that is a member of
+<code>examples.jini.sun.com</code>, so <code>kahlua</code> is the only 
+address in the drop-down list from the <b>Registrar</b> menu.
+<p>
+<img src="doc-files/browser4.gif"
+Alt="Picture of the lookup browser with drop-down registrar list visible.">
+<p>
+When <code>kahlua</code> is selected from the <b>Registrar</b>
+menu, the window contents change as shown in the next picture.
+In this example, six services are registered:
+<p>
+<img src="doc-files/browser5.gif"
+Alt="Picture of the lookup browser with six services visible.">
+
+<a name="inspecting"></a>
+<h2>Inspecting Services</h2>
+Double clicking on a service listed in the lower area of the main window
+will open a new window, which can be used to inspect and optionally edit
+attributes of the selected service. The window is titled <b>ServiceItem
+Editor</b> if the service's administrative proxy implements any of the
+administrative interfaces supported by the Service Browser, otherwise it is
+titled <b>ServiceItem Browser</b>. In our running example,
+double clicking on the <b>net.jini.core.lookup.ServiceRegistrar</b>
+item opens this window:
+<p>
+<img src="doc-files/browser6.gif"
+Alt="Picture of the ServiceItem editor when first opened.">
+<p>
+The window shows all of the attribute sets
+that can be inspected and, in some cases, altered. Attribute sets and
+attributes with blue icons can be edited, those with gray icons cannot.
+An attribute set can be edited if it is not
+{@link net.jini.lookup.entry.ServiceControlled} and the service's
+administrative proxy implements the {@link net.jini.admin.JoinAdmin} interface.
+Within an editable attribute set, an attribute can be edited if it is of
+type <code>String</code> or one of the primitive wrapper classes.
+The two attribute sets shown in the example above are <b>ServiceInfo</b>
+and <b>BasicServiceType</b>.
+Each of these entries can be expanded by double-clicking on them
+as illustrated in the next two pictures.
+<p>
+Expanding the <b>BasicServiceType</b> entry:
+<p>
+<img src="doc-files/browser7.gif"
+Alt="Picture of the ServiceItem editor with BasicServiceType expanded.">
+<p>
+Expanding the <b>ServiceInfo</b> entry:
+<p>
+<img src="doc-files/browser8.gif"
+Alt="Picture of the ServiceItem editor with all items expanded.">
+<p>
+Double clicking again on an entry collapses the view. In an expanded entry,
+double clicking on an editable attribute brings up a dialog box that
+allows you to input a new value for the attribute. You can also use
+the <b>Edit</b> menu in the <b>ServiceItem Editor</b> window to
+add and remove attribute sets, if the service's administrative proxy
+implements the {@link net.jini.admin.JoinAdmin} interface.
+Select <b>Add attribute...</b> from the <b>Edit</b> menu to add an 
+attribute set. (Note that for this to work, you will have to run the
+Service Browser in such a way that the desired attribute set classes
+are in both its class path and its codebase.)
+Click on an attribute set and then select <b>Remove attribute</b> from
+the <b>Edit</b> menu to delete an attribute set.
+
+<a name="filtering"></a>
+<h2>Filtering Services</h2>
+Back in the main window, the <b>Services</b> menu shows various types that
+the registered services are instances of. You can select one or more of
+these types to add them your current template to refine the set of
+services displayed in the lower area of the window. When you select a
+type, it appears in the upper text area to show you what your new
+template contains, the revised number of matching services is shown,
+and only the services that are instances of all of the selected types are
+displayed in the lower area.
+<p>
+Similarly, the <b>Attributes</b> menu shows attribute set classes (with
+package prefixes removed) that can be added to your current template to
+refine the set of services.
+When you select a class, it appears in the upper text area to 
+show you what your new template contains, the revised number of 
+matching services is shown, and only the services with such an attribute
+set are displayed in the lower area. In addition, the selected menu item
+changes to a  pull-aside menu.
+You can go back to the <b>Attributes</b> menu to select 
+additional attribute set classes, or traverse a pull-aside menu to
+see the names of attributes, each of which is itself a pull-aside 
+menu. You can traverse these to see the possible values for the 
+attributes, and select one to further refine your search. You
+can deselect existing attribute choices, and you can deselect 
+the <code>(match)</code> menu item to deselect an entire class. 
+<p>
+By default, the <b>Services</b> menu only shows service interface types. You 
+can select <b>Service classes</b> from the <b>Options</b> menu to see service
+implementation classes instead. By default, only the most-derived interfaces
+or classes are shown, but you can select <b>Service supertypes</b> from the
+<b>Options</b> menu to see supertypes as well. Similarly, by default only the 
+most-derived attribute set classes are shown in the <b>Attributes</b> 
+menu, but you can select <b>Attribute supertypes</b> from the 
+<b>Options</b> menu to see superclasses as well. 
+<p>
+Select <b>Reset</b> from the <b>File</b> menu of the main window
+to reset the template to the initial (empty) state.
+
+<a name="miscops"></a>
+<h2>Other Operations</h2>
+In the lower area of the main window, you can right click on a service
+to obtain a popup menu. Select <b>Show Info</b> from this menu to see
+details about service ID and service types. If the service's administrative
+proxy implements any of the administrative interfaces supported by the Service
+Browser, selecting <b>Admin Service</b> from this menu brings up the
+<b>ServiceItem Editor</b> window. Selecting <b>Browse Service</b> from
+this menu brings up the <b>ServiceItem Browser</b> window. If the service
+is a JavaSpaces service and either its proxy implements the
+{@link net.jini.space.JavaSpace05} interface or its administrative proxy
+implements the {@link com.sun.jini.outrigger.JavaSpaceAdmin}
+interface, selecting <b>Browse Entries</b> from this menu brings up a
+window that displays all of the current entries.
+<p>
+In a <b>ServiceItem Editor</b> window, the <b>Admin</b> menu allows
+various administrative operations to be performed on the service.
+If the service's administrative proxy implements the
+{@link net.jini.admin.JoinAdmin} interface,
+you can use the <b>Joining groups...</b> and <b>Joining locators...</b>
+menu items to control which lookup groups and lookup locators the
+service uses for lookup discovery and join. 
+If the service is itself a lookup service and its administrative proxy
+implements the {@link net.jini.lookup.DiscoveryAdmin} interface, you can use
+the <b>Member groups...</b> menu item to control 
+which groups the lookup service is a member of, and you can 
+use the <b>Unicast port...</b> menu item to control 
+which unicast port the lookup service uses for its lookup locator. 
+If the service's administrative proxy implements the
+{@link com.sun.jini.admin.DestroyAdmin}
+interface, you can use the <b>Destroy</b> menu item to destroy the
+service. This last menu selection is illustrated in the picture below.
+<p>
+<img src="doc-files/browser9.gif"
+Alt="Picture of the ServiceItem editor with Destroy selected from the Admin menu.">
+
+<a name="configuring"></a>
+<h2>Configuring the Service Browser</h2>
+
+The Service Browser obtains its configuration by calling
+{@link net.jini.config.ConfigurationProvider#getInstance(String[],ClassLoader)
+ConfigurationProvider.getInstance} with the <var><b>config_options</b></var>
+specified on the command line and the class loader for the main
+implementation class.
+<p>
+The Service Browser supports the following configuration
+entries, with component <code>com.sun.jini.example.browser</code>:
+<p>
+
+<a name="adminPreparer"></a>
+<table summary="Describes the adminPreparer configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    adminPreparer</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.security.ProxyPreparer}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>
+      new {@link net.jini.security.BasicProxyPreparer}()
+      </code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Prepares administrative proxies obtained by calling
+    the {@link net.jini.admin.Administrable#getAdmin Administrable.getAdmin}
+    method on service proxies. The value should not be <code>null</code>.
+    The Service Browser only calls methods on an administrative proxy if
+    administrative operations are performed using menu items. Methods of
+    the {@link net.jini.admin.JoinAdmin},
+    {@link net.jini.lookup.DiscoveryAdmin},
+    {@link com.sun.jini.admin.DestroyAdmin}, and
+    {@link com.sun.jini.outrigger.JavaSpaceAdmin} interfaces can be invoked.
+</table>
+
+<a name="discoveryManager"></a>
+<table summary="Describes the discoveryManager configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    discoveryManager</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.discovery.DiscoveryManagement}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td>
+<pre>
+new {@link net.jini.discovery.LookupDiscoveryManager}(
+    {@link net.jini.discovery.DiscoveryGroupManagement#NO_GROUPS},
+    null,  // locators
+    null,  // listener
+    this)  // config
+</pre>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Supplies the object used to discover lookup services.
+    The value should not be <code>null</code>. The object returned must
+    also implement {@link net.jini.discovery.DiscoveryGroupManagement} and
+    {@link net.jini.discovery.DiscoveryLocatorManagement}, and should be
+    configured initially to discover no groups and no specific lookup
+    services.
+</table>
+
+<a name="exitActionListener"></a>
+<table summary="Describes the exitActionListener configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    exitActionListener</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link java.awt.event.ActionListener}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> see the {@link com.sun.jini.example.browser.Browser}
+    constructors for details
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> The action listener for the <b>Exit</b> item of
+    the <b>File</b> menu.
+</table>
+
+<a name="folderView"></a>
+<table summary="Describes the folderView configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    folderView</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> <code>boolean</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>true</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Controls the format of the main window.
+    If <code>true</code>, the main window is split into two areas
+    as described in the rest of this documentation. If <code>false</code>,
+    the main window a single text area, and many of the browsing and
+    administrative operations are not available; the <b>Show Matches</b>
+    item of the <b>File</b> menu must be used to display services
+    registered in the selected lookup service.
+</table>
+
+<a name="initialLookupGroups"></a>
+<table summary="Describes the initialLookupGroups configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    initialLookupGroups</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> <code>String[]</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>null</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> The names of the lookup groups that the Service
+    Browser should initially discover. An empty array specifies that no groups
+    should be discovered. A <code>null</code> value specifies that all
+    lookup services should be discovered. Otherwise, the value specifies the
+    names of the groups to discover. The array must not contain
+    <code>null</code> elements. This entry used when the Service Browser is
+    first started; after initial startup, lookup service discovery is
+    controlled through the <b>File</b> menu.
+</table>
+
+<a name="initialLookupLocators"></a>
+<table summary="Describes the initialLookupLocators configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    initialLookupLocators</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> <code>{@link net.jini.core.discovery.LookupLocator}[]</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>null</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> The locators for specific lookup services that
+    the Service Browser should initially discover. An empty array or
+    <code>null</code> specifies that no specific lookup services
+    should be discovered. The array must not contain <code>null</code>
+    elements. This entry is used when the Service Browser is first started;
+    after initial startup, lookup service discovery is controlled through
+    the <b>File</b> menu.
+  <p>
+    Note that the constraints specified in the <code>locatorConstraints</code>
+    configuration entry will not be used for these initial locators; any
+    desired constraints should be attached directly to the initial locators.
+</table>
+
+<a name="leaseManager"></a>
+<table summary="Describes the leaseManager configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    leaseManager</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.lease.LeaseRenewalManager}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>new LeaseRenewalManager(this /* config */)</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Supplies the lease renewal manager used to
+    renew leases for remote event registrations and when obtaining the
+    entries of a JavaSpaces service. The value should not be
+    <code>null</code>.
+</table>
+
+<a name="leasePreparer"></a>
+<table summary="Describes the leasePreparer configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    leasePreparer</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.security.ProxyPreparer}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>
+      new {@link net.jini.security.BasicProxyPreparer}()
+      </code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Prepares leases obtained from registering
+    for event notifications with lookup services and from requesting
+    the entries of a JavaSpaces service. The value should not be
+    <code>null</code>.
+   <p>
+    The Service Browser calls the {@link net.jini.core.lease.Lease#renew
+    renew} and {@link net.jini.core.lease.Lease#cancel cancel} methods on
+    leases, and the {@link net.jini.core.lease.LeaseMap#renewAll renewAll}
+    method on lease maps created (via the {@link
+    net.jini.lease.LeaseRenewalManager}) from leases.
+</table>
+
+<a name="listenerExporter"></a>
+<table summary="Describes the listenerExporter configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    listenerExporter</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.export.Exporter}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td>
+<pre>
+new {@link net.jini.jeri.BasicJeriExporter}(
+    {@link net.jini.jeri.tcp.TcpServerEndpoint#getInstance TcpServerEndpoint.getInstance}(0),
+    new {@link net.jini.jeri.BasicILFactory}(), false, false)
+</pre>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> The object to use for exporting all
+    {@link net.jini.core.event.RemoteEventListener} instances used
+    by the Service Browser when registering for event notifications from
+    lookup services. The value should not be <code>null</code>.
+</table>
+
+<a name="locatorConstraints"></a>
+<table summary="Describes the locatorConstraints configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    locatorConstraints</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.core.constraint.MethodConstraints}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>null</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Constraints to attach to any
+    {@link net.jini.core.discovery.LookupLocator} instances constructed
+    from the addresses entered using the <b>Find By Address...</b> menu item.
+   <p>
+    These constraints are not attached to the locators obtained from the
+    <code>initialLookupLocators</code> configuration entry.
+</table>
+
+<a name="loginContext"></a>
+<table summary="Describes the loginContext configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    loginContext</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link javax.security.auth.login.LoginContext}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>null</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Specifies the JAAS login context to use for
+    performing a JAAS login and supplying the
+    {@link javax.security.auth.Subject} to use when running the
+    Service Browser. If <code>null</code>, no JAAS login is performed
+    and no subject is used.
+</table>
+
+<a name="servicePreparer"></a>
+<table summary="Describes the servicePreparer configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    servicePreparer</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> {@link net.jini.security.ProxyPreparer}
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>
+      new {@link net.jini.security.BasicProxyPreparer}()
+      </code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Prepares service proxies obtained from lookup servics.
+    The value should not be <code>null</code>. The Service Browser
+    calls the {@link net.jini.admin.Administrable#getAdmin
+    Administrable.getAdmin} method on a service proxy for most
+    administrative operations, and calls the
+    {@link net.jini.space.JavaSpace05#contents JavaSpace05.contents}
+    method on a JavaSpaces service proxy to browse entries.
+</table>
+
+<a name="uninterestingInterfaces"></a>
+<table summary="Describes the uninterestingInterfaces configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    uninterestingInterfaces</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> <code>String[]</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td>
+<pre>
+new String[]{
+    "java.io.Serializable",
+    "java.rmi.Remote",
+    "net.jini.admin.Administrable",
+    "net.jini.core.constraint.RemoteMethodControl",
+    "net.jini.id.ReferentUuid"
+    "net.jini.security.proxytrust.TrustEquivalence"}
+</pre>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Supplies the set of interface names that are not
+    interesting to choose when displaying service instances the lower
+    area of the main window. If the service proxy implements exactly
+    one interface that is not in this list, the name of that interface
+    is used to name the service, otherwise the concrete class of the
+    service proxy is used appended with all the names of the 'interesting'
+    interfaces implemented by that proxy. The value should not be
+    <code>null</code>.
+</table>
+
+<a name="autoConfirm"></a>
+<table summary="Describes the autoConfirm configuration entry"
+       border="0" cellpadding="2">
+  <tr valign="top">
+    <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
+    <th scope="col" align="left" colspan="2"> <font size="+1"><code>
+    autoConfirm</code></font>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Type: <td> <code>boolean</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Default: <td> <code>false</code>
+  <tr valign="top"> <td> &nbsp <th scope="row" align="right">
+    Description: <td> Controls whether a confirmation window must popup that
+    requires a confirmation for the update or removal of the service editor
+    window. In case set to <code>true</code> closing of the service editor
+    window will occur automatically in case a service is removed, in case of
+    modifications to the lookup attributes of a service the service editor
+    window will be updated.
+</table>
+<p>
+Note that additional entries can also be specified to configure the
+{@link net.jini.lease.LeaseRenewalManager},
+{@link net.jini.discovery.LookupDiscovery}, and
+{@link net.jini.discovery.LookupLocatorDiscovery} components. For
+example, the Service Browser assumes you have configured the discovery
+manager (obtained from the <code>discoveryManager</code> configuration
+entry) to enforce any desired constraints for multicast discovery
+and to perform any desired proxy preparation for all discovered lookup
+services.
+<p>
+The most commonly used configuration entries are likely to be:
+<ul>
+<li><a href="#initialLookupGroups"><code>initialLookupGroups</code></a>
+<li><a href="#initialLookupLocators"><code>initialLookupLocators</code></a>
+</ul>
+<p>
+Secure deployments typically will provide values for most of the following
+configuration entries:
+<ul>
+<li><a href="#loginContext"><code>loginContext</code></a>
+<li><a href="#leasePreparer"><code>leasePreparer</code></a>
+<li><a href="#servicePreparer"><code>servicePreparer</code></a>
+<li><a href="#adminPreparer"><code>adminPreparer</code></a>
+<li><a href="#listenerExporter"><code>listenerExporter</code></a>
+<li><a href="#locatorConstraints"><code>locatorConstraints</code></a>
+</ul>
+plus the following configuration entries for <code>LookupDiscovery</code>
+(if groups are used for discovery):
+<ul>
+<li><code>registrarPreparer</code></li>
+<li><code>discoveryConstraints</code></li>
+</ul>
+and the following configuration entry for <code>LookupLocatorDiscovery</code>
+(if locators are used for discovery):
+<ul>
+<li><code>registrarPreparer</code></li>
+</ul>
+
+<a name="logging"></a>
+<h2>Loggers and Logging Levels</h2>
+
+The Service Browser uses the {@link java.util.logging.Logger} named
+<code>com.sun.jini.example.browser</code> to log
+information at the following logging levels:
+<p>
+<table border="1" cellpadding="5"
+       summary="Describes logging performed by Service Browser at different
+       logging levels">
+
+<caption halign="center" valign="top"><b><code>
+	com.sun.jini.example.browser</code></b></caption>
+
+<tr> <th scope="col"> Level <th scope="col"> Description
+
+<tr> <td> {@link java.util.logging.Level#SEVERE SEVERE} <td>
+Problems that prevent Service Browser startup
+
+<tr> <td> {@link java.util.logging.Level#INFO INFO} <td> Potentially
+interesting exceptions that occur when performing operations
+
+<tr> <td> {@link com.sun.jini.logging.Levels#HANDLED HANDLED} <td>
+Less interesting exceptions that occur when performing operations
+
+</table>
+<p>
+See the {@link com.sun.jini.logging.LogManager} class for one way to use the
+<code>HANDLED</code> logging level in standard logging configuration files.
+
+<a name="examples"></a>
+<h2>Examples for Running the Service Browser</h2>
+
+An example command line for starting the Service Browser is:
+<blockquote><pre>
+% java -Djava.security.policy=<a href="#policy">browser.policy</a>
+       -Djava.rmi.server.codebase="http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/browser-dl.jar http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/jsk-dl.jar"
+       -Djava.protocol.handler.pkgs=net.jini.url
+       -jar <var><b>install_dir</b></var>/lib/browser.jar
+</pre></blockquote>
+In this example, the local host is assumed to have the name
+<var><b>your_host</b></var>, running an HTTP server on port
+<var><b>http_port</b></var>, serving files in the <code>lib</code>
+subdirectory of <var><b>install_dir</b></var>, the directory where the
+Apache River release is installed.
+When the Service Browser is run this way, without a configuration file, it
+will perform multicast discovery to find all available lookup
+services, and will use Jini extensible remote invocation (Jini ERI)
+over TCP/IP for its remote event listeners.
+<p>
+An example command line for starting the Service Browser with a
+configuration file is:
+<blockquote><pre>
+% java -Djava.security.policy=<a href="#policy">browser.policy</a>
+       -Djava.rmi.server.codebase="http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/browser-dl.jar http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/jsk-dl.jar"
+       -Djava.protocol.handler.pkgs=net.jini.url
+       -jar <var><b>install_dir</b></var>/lib/browser.jar
+       <a href="#public">public.config</a>
+</pre></blockquote>
+The example configuration file <code>public.config</code> only discovers
+lookup services in the public group:
+<a name="public"></a>
+<blockquote><pre>
+com.sun.jini.example.browser {
+    initialLookupGroups = new String[]{""};
+}
+</pre></blockquote>
+<p>
+The following example configuration file initially discovers no
+lookup services and uses JRMP for its remote event listeners:
+<blockquote><pre>
+com.sun.jini.example.browser {
+    initialLookupGroups = new String[]{};
+    listenerExporter = new net.jini.jrmp.JrmpExporter();
+}
+</pre></blockquote>
+<p>
+In all the above command lines, the policy file <code>browser.policy</code>
+was specified for the <code>java.security.policy</code> system property.
+An example policy file is:
+<a name="policy"></a>
+<blockquote><pre>
+grant codebase "file:<var><b>install_dir</b></var>/lib/browser.jar" {
+    permission java.security.AllPermission;
+};
+
+grant codebase "file:<var><b>install_dir</b></var>/lib/jsk-platform.jar" {
+    permission java.security.AllPermission;
+};
+
+grant codebase "file:<var><b>install_dir</b></var>/lib/jsk-lib.jar" {
+    permission java.security.AllPermission;
+};
+
+grant {
+    permission java.net.SocketPermission "*:1024-", "connect";
+};
+</pre></blockquote>
+<p>
+<b>Note:</b> Of course, a more restrictive policy file could be used;
+the one shown here is for illustrative purposes only.
+<p>
+An example of running under the Service Starter is:
+<blockquote><pre>
+% java -Djava.security.policy=<a href="#start_policy">start.policy</a>
+       -Djava.protocol.handler.pkgs=net.jini.url
+       -jar <var><b>install_dir</b></var>/lib/start.jar
+       <a href="#start_config">start.config</a>
+</pre></blockquote>
+<p>
+where <code>start.config</code> is the following configuration file:
+<a name="start_config"></a>
+<blockquote><pre>
+import com.sun.jini.start.NonActivatableServiceDescriptor;
+import com.sun.jini.start.ServiceDescriptor;
+
+com.sun.jini.start {
+
+  serviceDescriptors = new ServiceDescriptor[]{
+    new NonActivatableServiceDescriptor(
+      "http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/browser-dl.jar http://<var><b>your_host</b></var>:<var><b>http_port</b></var>/jsk-dl.jar",
+      "<a href="#policy">browser.policy</a>",
+      "<var><b>install_dir</b></var>/lib/browser.jar",
+      "com.sun.jini.example.browser.Browser",
+      new String[]{"<a href="#public">public.config</a>"})
+    };
+}
+</pre></blockquote>
+An example policy file for <code>start.policy</code> is:
+<a name="start_policy"></a>
+<blockquote><pre>
+grant codebase "file:<var><b>install_dir</b></var>/lib/start.jar" {
+    permission java.security.AllPermission;
+};
+
+grant codebase "file:<var><b>install_dir</b></var>/lib/jsk-platform.jar" {
+    permission java.security.AllPermission;
+};
+</pre></blockquote>
+</body>
+</html>
diff --git a/browser/src/test/java/org/apache/river/container/browser/AppTest.java b/browser/src/test/java/org/apache/river/container/browser/AppTest.java
new file mode 100644
index 0000000..f1d7161
--- /dev/null
+++ b/browser/src/test/java/org/apache/river/container/browser/AppTest.java
@@ -0,0 +1,38 @@
+package org.apache.river.container.browser;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Unit test for simple App.
+ */
+public class AppTest 
+    extends TestCase
+{
+    /**
+     * Create the test case
+     *
+     * @param testName name of the test case
+     */
+    public AppTest( String testName )
+    {
+        super( testName );
+    }
+
+    /**
+     * @return the suite of tests being tested
+     */
+    public static Test suite()
+    {
+        return new TestSuite( AppTest.class );
+    }
+
+    /**
+     * Rigourous Test :-)
+     */
+    public void testApp()
+    {
+        assertTrue( true );
+    }
+}
diff --git a/pom.xml b/pom.xml
index b0cfd61..0b0f4ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,8 @@
         <module>test-container</module>
         <module>reggie-module</module>
         <module>browser-module</module>
-    </modules>
+    <module>browser</module>
+  </modules>
   
     <build>
         <pluginManagement>
diff --git a/river-container-core/src/main/java/org/apache/river/container/deployer/StarterServiceDeployer.java b/river-container-core/src/main/java/org/apache/river/container/deployer/StarterServiceDeployer.java
index 46911e3..d98ddd1 100644
--- a/river-container-core/src/main/java/org/apache/river/container/deployer/StarterServiceDeployer.java
+++ b/river-container-core/src/main/java/org/apache/river/container/deployer/StarterServiceDeployer.java
@@ -79,7 +79,6 @@
     private String myName = null;
     @Injected(style = InjectionStyle.BY_TYPE)
     private CodebaseHandler codebaseHandler = null;
-    
     private String config = Strings.STARTER_SERVICE_DEPLOYER_CONFIG;
     private ASTconfig configNode = null;
 
@@ -92,23 +91,19 @@
     }
     @Injected(style = InjectionStyle.BY_TYPE)
     private PropertiesFileReader propertiesFileReader = null;
-    
     @Injected(style = InjectionStyle.BY_TYPE)
     private ArgsParser argsParser = null;
-    
     @Injected(style = InjectionStyle.BY_TYPE)
     WorkManager workManager = null;
-    
-    @Injected(style=InjectionStyle.BY_TYPE)
-    ContextualWorkManager contextualWorkManager=null;
-    
+    @Injected(style = InjectionStyle.BY_TYPE)
+    ContextualWorkManager contextualWorkManager = null;
     @Injected(style = InjectionStyle.BY_TYPE)
     private DynamicPolicyProvider securityPolicy = null;
 
     public void addPlatformCodebaseJars(CodebaseContext codebaseContext) throws IOException {
         ASTcodebase codebaseNode = (ASTcodebase) configNode.search(new Class[]{
-                    ASTconfig.class, ASTclassloader.class, ASTcodebase.class
-                }).get(0);
+            ASTconfig.class, ASTclassloader.class, ASTcodebase.class
+        }).get(0);
         /*
          Register the platform codebase jars with the codebase service.
          */
@@ -143,7 +138,7 @@
          Include platform jars from the container's lib directory.
          */
         ASTclasspath platformJarSpec = (ASTclasspath) configNode.search(new Class[]{ASTconfig.class,
-                    ASTclassloader.class, ASTjars.class, ASTclasspath.class}).get(0);
+            ASTclassloader.class, ASTjars.class, ASTclasspath.class}).get(0);
         addPlatformJarsToClassloader(platformJarSpec, cl);
         addLibDirectoryJarsToClasspath(serviceRoot, cl);
 
@@ -184,10 +179,13 @@
          Register the service's codebase jars with the codebase service.
          */
         FileObject libDlDir = serviceRoot.resolveFile(Strings.LIB_DL);
-        List<FileObject> dljarFiles = Utils.findChildrenWithSuffix(libDlDir,
-                Strings.DOT_JAR);
-        for (FileObject jarFile : dljarFiles) {
-            codebaseContext.addFile(jarFile);
+        /* Don't bother if there is no lib-dl (e.g. for simple clients) */
+        if (libDlDir.exists()) {
+            List<FileObject> dljarFiles = Utils.findChildrenWithSuffix(libDlDir,
+                    Strings.DOT_JAR);
+            for (FileObject jarFile : dljarFiles) {
+                codebaseContext.addFile(jarFile);
+            }
         }
     }
 
@@ -209,10 +207,9 @@
          Launch the service.
          */
         log.log(Level.FINE, MessageNames.CALLING_MAIN, new Object[]{
-                    startClassName, Utils.format(args)
-                });
+            startClassName, Utils.format(args)
+        });
         Runnable task = new Runnable() {
-
             @Override
             public void run() {
                 try {
@@ -235,7 +232,7 @@
             throw new LocalizedRuntimeException(MessageNames.BUNDLE_NAME,
                     MessageNames.CANT_READ_START_PROPERTIES,
                     new Object[]{Strings.START_PROPERTIES,
-                        serviceRoot.getName().getBaseName()});
+                serviceRoot.getName().getBaseName()});
         }
         Properties startProps = propertiesFileReader.getProperties(startProperties);
         return startProps;
@@ -245,7 +242,7 @@
         /*
          Setup the liaison configuration.
          */
-        ClassLoader originalContextCl=Thread.currentThread().getContextClassLoader();
+        ClassLoader originalContextCl = Thread.currentThread().getContextClassLoader();
         try {
             Thread.currentThread().setContextClassLoader(cl);
             File workingDir = null;
@@ -299,8 +296,8 @@
         FileObject configFile = fileUtility.getProfileDirectory().resolveFile(config);
         InputStream in = configFile.getContent().getInputStream();
         configNode = DeployerConfigParser.parseConfig(in);
-        log.log(Level.FINE,MessageNames.STARTER_SERVICE_DEPLOYER_INITIALIZED, 
-                new Object[] {myName} );
+        log.log(Level.FINE, MessageNames.STARTER_SERVICE_DEPLOYER_INITIALIZED,
+                new Object[]{myName});
     }
 
     public ServiceLifeCycle deployServiceArchive(FileObject serviceArchive) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
@@ -311,7 +308,7 @@
                 serviceArchive.getFileSystem().getFileSystemManager().createFileSystem(Strings.JAR, serviceArchive));
         String serviceName = findServiceName(env.getServiceArchive(), env.getServiceRoot());
         env.setServiceName(serviceName);
-        ServiceLifeCycle slc=StarterServiceLifeCycleSM.newStarterServiceLifeCycle(env, this);
+        ServiceLifeCycle slc = StarterServiceLifeCycleSM.newStarterServiceLifeCycle(env, this);
         return slc;
     }
 
@@ -330,7 +327,7 @@
     }
 
     void prepareService(ApplicationEnvironment env) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
-        
+
         CodeSource serviceCodeSource =
                 new CodeSource(findServiceURL(env.getServiceArchive(), env.getServiceRoot()),
                 new Certificate[0]);
@@ -338,7 +335,7 @@
                 new Object[]{env.getServiceName(), serviceCodeSource});
         VirtualFileSystemClassLoader cl = createServiceClassloader(env.getServiceRoot(), serviceCodeSource);
         env.setClassLoader(cl);
-        
+
         /*
          Create a codebase context.
          */
@@ -359,13 +356,13 @@
         Permission[] perms = createPermissionsInClassloader(cl);
         grantPermissions(cl, perms);
         setupLiaisonConfiguration(env.getServiceArchive(), env.getServiceRoot(), cl);
-        
+
         /*
          * Create a working context (work manager).
          */
         env.setWorkingContext(contextualWorkManager.createContext(env.getServiceName()));
     }
-    
+
     void launchService(ApplicationEnvironment env, String[] serviceArgs) throws FileSystemException, IOException {
         Properties startProps = readStartProperties(env.getServiceRoot());
         String argLine = startProps.getProperty(Strings.START_PARAMETERS);
@@ -451,27 +448,27 @@
         constructor.setAccessible(true);
         return constructor.newInstance(parms, null);
     }
-    
+
     /**
-     * Attempt to stop the service in an orderly fashion.
-     * Go to the service, see if it implements Administrable, then get the 
-     * admin proxy and see if it implements DestroyAdmin.  If so, call it.
-     * @param env 
+     * Attempt to stop the service in an orderly fashion. Go to the service, see
+     * if it implements Administrable, then get the admin proxy and see if it
+     * implements DestroyAdmin. If so, call it.
+     *
+     * @param env
      */
-    public void stopService(ApplicationEnvironment env)  {
+    public void stopService(ApplicationEnvironment env) {
         /* Option 1 - Service has a getAdmin() method - it probably implements
          * Administrable.
          */
-        Object serviceInstance=env.getServiceInstance();
-        Method getAdmin=null;
+        Object serviceInstance = env.getServiceInstance();
+        Method getAdmin = null;
         try {
-            getAdmin=serviceInstance.getClass().getMethod(Strings.GET_ADMIN, new Class[0]);
+            getAdmin = serviceInstance.getClass().getMethod(Strings.GET_ADMIN, new Class[0]);
         } catch (Exception ex) {
             // Silent catch - leave it null;
         }
         if (getAdmin != null) {
-            
         }
-        
+
     }
 }