FileDocCategorySizeDatePackage
ContentHandlerImpl.javaAPI DocphoneME MR2 API (J2ME)32808Wed May 02 18:00:44 BST 2007com.sun.midp.content

ContentHandlerImpl.java

/*
 *
 *
 * Copyright  1990-2007 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt).
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions.
 */

package com.sun.midp.content;

import javax.microedition.content.ContentHandler;
import javax.microedition.content.ContentHandlerServer;
import javax.microedition.content.RequestListener;
import javax.microedition.content.ActionNameMap;
import javax.microedition.content.Invocation;

import java.io.DataInputStream;
import java.io.DataOutputStream;

/**
 * The internal structure of a registered content handler.
 */
public class ContentHandlerImpl implements ContentHandler {

    /**
     * The content handler ID.
     * Lengths up to 256 characters MUST be supported.
     * The ID may be <code>null</code>.
     */
    String ID;

    /**
     * The types that are supported by this content handler.
     * If there are no types registered, the this field MUST either be
     * <code>null</code> or refer to an empty array .
     */
    private String[] types;

    /**
     * The suffixes of URLs that are supported by this content handler.
     * If there are no suffixes, then this field MUST be <code>null</code>.
     * The suffixes MUST include any and all punctuation. For example,
     * the string <code>".png"</code>.
     */
    private String[] suffixes;

    /**
     * The actions that are supported by this content handler.
     * If there are no actions, then this field MSUT be <code>null</code>.
     */
    private String[] actions;

    /**
     * The action names that are defined by this content handler.
     */
    ActionNameMap[] actionnames;

    /** The sequence number of this instance; monotonic increasing. */
    final int seqno;

    /** The next sequence number to assign. */
    private static int nextSeqno;

    /**
     * The RequestListenerImpl; if a listener is set.
     */
    RequestListenerImpl listenerImpl;

    /** Empty String array to return when needed. */
    public final static String[] ZERO_STRINGS = {};

    /** Empty ActionNameMap to return when needed. */
    private final static ActionNameMap[] ZERO_ACTIONNAMES =
        new ActionNameMap[0];

    /** Property name for the current locale. */
    private final static String LOCALE_PROP = "microedition.locale";

    /**
     * The MIDlet suite storagename that contains the MIDlet.
     */
    int storageId;

    /**
     * The Name from parsing the Property for the MIDlet
     * with this classname.
     */
    String appname;

    /**
     * The Version parsed from MIDlet-Version attribute.
     */
    String version;

    /**
     * The application class name that implements this content
     * handler.  Note: Only the application that registered the class
     * will see the classname; for other applications the value will be
     * <code>null</code>.
     */
    String classname;

    /**
     * The authority that authenticated this ContentHandler.
     */
    String authority;

    /**
     * The accessRestrictions for this ContentHandler.
     */
    private String[] accessRestricted;

    /**
     * Indicates content handler registration method:
     * dynamic registration from the API, static registration from install
     * or native content handler.
     * Must be similar to enum in jsr211_registry.h
     */
    int registrationMethod;

    /** Content handler statically registered during installation */
    final static int REGISTERED_STATIC = 0;
    /** Dynamically registered content handler via API */
    final static int REGISTERED_DYNAMIC = 1;
    /** Native platform content handler  */
    final static int REGISTERED_NATIVE  = 2;

    /** Count of requests retrieved via {@link #getRequest}. */
    int requestCalls;

    /**
     * Instance is a registration or unregistration.
     * An unregistration needs only storageId and classname.
     */
    boolean removed;

    /**
     * Construct a ContentHandlerImpl.
     * Verifies that all strings are non-null
     * @param types an array of types to register; may be
     *  <code>null</code>
     * @param suffixes an array of suffixes to register; may be
     *  <code>null</code>
     * @param actions an array of actions to register; may be
     *  <code>null</code>
     * @param actionnames an array of ActionNameMaps to register; may be
     *  <code>null</code>
     * @param ID the content handler ID; may be <code>null</code>
     * @param accessRestricted the  IDs of applications allowed access
     * @param auth application authority
     *
     * @exception NullPointerException if any types, suffixes,
     *   actions, actionnames array element is null
     *
     * @exception IllegalArgumentException is thrown if any of
     *   the types, suffix, or action strings have a
     *   length of zero or
     *   if the ID has a length of zero or contains any
     *        control character or space (U+0000-U+00020)
     */
    ContentHandlerImpl(String[] types, String[] suffixes,
                       String[] actions, ActionNameMap[] actionnames,
                       String ID, String[] accessRestricted, String auth) {
        this();

        // Verify consistency between actions and ActionNameMaps
        if (actionnames != null && actionnames.length > 0) {
            if (actions == null) {
                throw new IllegalArgumentException("no actions");
            }
            int len = actions.length;
            for (int i = 0; i < actionnames.length; i++) {
                // Verify the actions are the same
                ActionNameMap map = actionnames[i];
                if (len != map.size()) {
                    throw new IllegalArgumentException("actions not identical");
                }

                for (int j = 0; j < len; j++) {
                    if (!actions[j].equals(map.getAction(j))) {
                        throw new
                            IllegalArgumentException("actions not identical");
                    }
                }

                /*
                 * Verify the locale of this ActionNameMap is not the same
                 * as any previous ActionNameMap.
                 */
                for (int j = 0; j < i; j++) {
                    if (map.getLocale().equals(actionnames[j].getLocale())) {
                        throw new IllegalArgumentException("duplicate locale");
                    }
                }
            }
        }

        // Check the ID for invalid characters (controls or space)
        if (ID != null) {
            int len = ID.length();
            if (len == 0) {
                    throw new IllegalArgumentException("invalid ID");
            }
            for (int i = 0; i < ID.length(); i++) {
                if (ID.charAt(i) <= 0x0020) {
                    throw new IllegalArgumentException("invalid ID");
                }
            }
            this.ID = ID;
        }
        this.types = copy(types);
        this.suffixes = copy(suffixes);
        this.actions = copy(actions);
        this.actionnames = copy(actionnames);
        this.accessRestricted = copy(accessRestricted);
        this.authority = auth;
    }

    /**
     * Initialize a new instance with the same information.
     * @param handler another ContentHandlerImpl
     * @see javax.microedition.content.ContentHandlerServerImpl
     */
    protected ContentHandlerImpl(ContentHandlerImpl handler) {
        this();
        types = handler.types;
        suffixes = handler.suffixes;
        ID = handler.ID;
        accessRestricted = handler.accessRestricted;
        actions = handler.actions;
        actionnames = handler.actionnames;
        listenerImpl = handler.listenerImpl;
        storageId = handler.storageId;
        classname = handler.classname;
        version = handler.version;
        registrationMethod = handler.registrationMethod;
        requestCalls = handler.requestCalls;
        authority = handler.authority;
        appname = handler.appname;
    }

    /**
     * Constructor used to read handlers.
     */
    ContentHandlerImpl() {
        seqno = nextSeqno++;
    }

    /**
     * Checks that all of the string references are non-null
     * and not zero length.  If either the argument is null or
     * is an empty array the default ZERO length string array is used.
     *
     * @param strings array to check for null and length == 0
     * @return a non-null array of strings; an empty array replaces null
     * @exception NullPointerException if any string ref is null
     * @exception IllegalArgumentException if any string
     * has length == 0
     */
    public static String[] copy(String[] strings) {
        if (strings != null && strings.length > 0) {
            String[] copy = new String[strings.length];
            for (int i = 0; i < strings.length; i++) {
                if (strings[i].length() == 0) {
                    throw new IllegalArgumentException("string length is 0");
                }
                copy[i] = strings[i];

            }
            return strings;
        } else {
            return ZERO_STRINGS;
        }
    }
    /**
     * Checks that all of the actionname references are non-null.
     *
     * @param actionnames array to check for null and length == 0
     * @return a non-null array of actionnames; an empty array replaces null
     * @exception NullPointerException if any string ref is null
     */
    private static ActionNameMap[] copy(ActionNameMap[] actionnames) {
        if (actionnames != null && actionnames.length > 0) {
            ActionNameMap[] copy = new ActionNameMap[actionnames.length];
            for (int i = 0; i < actionnames.length; i++) {
                // Check for null
                if (actionnames[i] == null) {
                    throw new NullPointerException();
                }
                copy[i] = actionnames[i];
            }
            return copy;
        } else {
            return ZERO_ACTIONNAMES;
        }
    }

    /**
     * Copy an array of ContentHandlers making a new ContentHandler
     * for each ContentHandler.  Make copies of any mutiple object.
     * @param handlers the array of handlers duplicate
     * @return the new array of content handlers
     */
    public static ContentHandler[] copy(ContentHandler[] handlers) {
        ContentHandler[] h = new ContentHandler[handlers.length];
        for (int i = 0; i < handlers.length; i++) {
            h[i] = handlers[i];
        }
        return h;
    }

    /**
     * Get the nth type supported by the content handler.
     * @param index the index into the types
     * @return the nth type
     * @exception IndexOutOfBounds if index is less than zero or
     *     greater than or equal to the value of the
     *     {@link #getTypeCount getTypeCount} method.
     */
    public String getType(int index) {
        return get(index, getTypes());
    }


    /**
     * Get the number of types supported by the content handler.
     *
     * @return the number of types
     */
    public int getTypeCount() {
        return getTypes().length;
    }

    /**
     * Get types supported by the content handler.
     *
     * @return array of types supported
     */
    String [] getTypes() {
        if (types == null) {
            types = RegistryStore.getArrayField(ID, RegistryStore.FIELD_TYPES);
        }
        return types;
    }

    /**
     * Determine if a type is supported by the content handler.
     *
     * @param type the type to check for
     * @return <code>true</code> if the type is supported;
     *  <code>false</code> otherwise
     * @exception NullPointerException if <code>type</code>
     * is <code>null</code>
     */
    public boolean hasType(String type) {
        return has(type, getTypes(), true);
    }

    /**
     * Get the nth suffix supported by the content handler.
     * @param index the index into the suffixes
     * @return the nth suffix
     * @exception IndexOutOfBounds if index is less than zero or
     *     greater than or equal to the value of the
     *     {@link #getSuffixCount getSuffixCount} method.
     */
    public String getSuffix(int index) {
        return get(index, getSuffixes());
    }

    /**
     * Get the number of suffixes supported by the content handler.
     *
     * @return the number of suffixes
     */
    public int getSuffixCount() {
        return getSuffixes().length;
    }

    /**
     * Determine if a suffix is supported by the content handler.
     *
     * @param suffix the suffix to check for
     * @return <code>true</code> if the suffix is supported;
     *  <code>false</code> otherwise
     * @exception NullPointerException if <code>suffix</code>
     * is <code>null</code>
     */
    public boolean hasSuffix(String suffix) {
        return has(suffix, getSuffixes(), true);
    }

    /**
     * Get suffixes supported by the content handler.
     *
     * @return array of suffixes supported
     */
    String [] getSuffixes() {
        if (suffixes == null) {
            suffixes =
                RegistryStore.getArrayField(ID, RegistryStore.FIELD_SUFFIXES);
        }
        return suffixes;
    }

    /**
     * Get the nth action supported by the content handler.
     * @param index the index into the actions
     * @return the nth action
     * @exception IndexOutOfBounds if index is less than zero or
     *     greater than or equal to the value of the
     *     {@link #getActionCount getActionCount} method.
     */
    public String getAction(int index) {
        return get(index, getActions());
    }

    /**
     * Get the number of actions supported by the content handler.
     *
     * @return the number of actions
     */
    public int getActionCount() {
        return getActions().length;
    }

    /**
     * Determine if a action is supported by the content handler.
     *
     * @param action the action to check for
     * @return <code>true</code> if the action is supported;
     *  <code>false</code> otherwise
     * @exception NullPointerException if <code>action</code>
     * is <code>null</code>
     */
    public boolean hasAction(String action) {
        return has(action, getActions(), false);
    }

    /**
     * Get actions supported by the content handler.
     *
     * @return array of actions supported
     */
    String [] getActions() {
        if (actions == null) {
            actions =
                RegistryStore.getArrayField(ID, RegistryStore.FIELD_ACTIONS);
        }
        return actions;
    }

    /**
     * Gets the value at index in the string array.
     * @param index of the value
     * @param strings array of strings to get from
     * @return string at index.
     * @exception IndexOutOfBounds if index is less than zero or
     *     greater than or equal length of the array.
     */
    private String get(int index, String[] strings) {
        if (index < 0 || index >= strings.length) {
            throw new IndexOutOfBoundsException();
        }
        return strings[index];
    }

    /**
     * Determines if the string is in the array.
     * @param string to locate
     * @param strings array of strings to get from
     * @param ignoreCase true to ignore case in matching
     * @return <code>true</code> if the value is found
     * @exception NullPointerException if <code>string</code>
     * is <code>null</code>
     */
    private boolean has(String string, String[] strings, boolean ignoreCase) {
        int len = string.length(); // Throw NPE if null
        for (int i = 0; i < strings.length; i++) {
            if (strings[i].length() == len &&
                string.regionMatches(ignoreCase, 0, strings[i], 0, len)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the mapping of actions to action names for the current
     * locale supported by this content handler. The behavior is
     * the same as invoking {@link #getActionNameMap} with the current
     * locale.
     *
     * @return an ActionNameMap; if there is no map available for the
     *  current locale, then it MUST be <code>null</code>
     */
    public ActionNameMap getActionNameMap() {
        String locale = System.getProperty(LOCALE_PROP);
        return (locale == null) ? null : getActionNameMap(locale);
    }

    /**
     * Get the mapping of actions to action names for the requested
     * locale supported by this content handler.
     * The locale is matched against the available locales.
     * If a match is found it is used.  If an exact match is
     * not found, then the locale string is shortened and retried
     * if either of the "_" or "-" delimiters is present.
     * The locale is shortened by retaining only the characters up to
     * but not including the last occurence of the delimiter
     * (either "_" or "-").
     * The shortening and matching is repeated as long as the string
     * contains one of the delimiters.
     * Effectively, this will try the full locale and then try
     * without the variant or country code, if they were present.
     *
     * @param locale for which to find an ActionNameMap;
     *   MUST NOT be <code>null</code>
     * @return an ActionNameMap; if there is no map available for the
     *  locale, then it MUST be <code>null</code>
     * @exception NullPointerException if the locale is <code>null</code>
     */
    public ActionNameMap getActionNameMap(String locale) {
        while (locale.length() > 0) {
            for (int i = 0; i < getActionNames().length; i++) {
                if (locale.equals(getActionNames()[i].getLocale())) {
                    return getActionNames()[i];
                }
            }
            int lastdash = locale.lastIndexOf('-');
            if (lastdash < 0) {
                break;
            }
            locale = locale.substring(0, lastdash);
        }
        return null;
    }

    /**
     * Gets the number of action name maps supported by the content handler.
     *
     * @return the number of action name maps
     */
    public int getActionNameMapCount() {
        return getActionNames().length;
    }

    /**
     * Gets the n<sup>th</sup> ActionNameMap supported by the
     * content handler.
     * @param index the index of the locale
     * @return the n<sup>th</sup> ActionNameMap
     *
     * @exception IndexOutOfBoundsException if index is less than zero or
     *     greater than or equal to the value of the
     *     {@link #getActionNameMapCount getActionNameMapCount} method.
     */
    public ActionNameMap getActionNameMap(int index) {
        if (index < 0 || index >= getActionNames().length) {
            throw new IndexOutOfBoundsException();
        }
        return getActionNames()[index];
    }

    /**
     * Get actions names for the content handler.
     *
     * @return array of actions names
     */
    private ActionNameMap[] getActionNames() {
        if (actionnames == null) {
            String [] locales =
                RegistryStore.getArrayField(ID, RegistryStore.FIELD_LOCALES);
            String [] names   =
                RegistryStore.getArrayField(ID, RegistryStore.FIELD_ACTION_MAP);

            actionnames = new ActionNameMap[locales.length];
            for (int index = 0; index < locales.length; index++) {
                String [] temp = new String[getActions().length];

                System.arraycopy(names,
                                 index * getActions().length,
                                 temp,
                                 0,
                                 getActions().length);

                actionnames[index] = new ActionNameMap(getActions(),
                                                       temp,
                                                       locales[index]);
            }
        }
        return actionnames;
    }

    /**
     * Returns the name used to present this content handler to a user.
     * The value is extracted from the normal installation information
     * about the content handler application.
     *
     * @return the user-friendly name of the application;
     * it MUST NOT be <code>null</code>
     */
    public String getAppName() {
        loadAppData();
        return appname;
    }

    /**
     * Gets the version number of this content handler.
     * The value is extracted from the normal installation information
     * about the content handler application.
     * @return the version number of the application;
     * MAY be <code>null</code>
     */
    public String getVersion() {
        loadAppData();
        return version;
    }

    /**
     * Get the content handler ID.  The ID uniquely identifies the
     * application which contains the content handler.
     * After registration and for every registered handler,
     * the ID MUST NOT be <code>null</code>.
     * @return the ID; MUST NOT be <code>null</code> unless the
     *  ContentHandler is not registered.
     */
    public String getID() {
        return ID;
    }

    /**
     * Gets the name of the authority that authorized this application.
     * This value MUST be <code>null</code> unless the device has been
     * able to authenticate this application.
     * If <code>non-null</code>, it is the string identifiying the
     * authority.  For example,
     * if the application was a signed MIDlet, then this is the
     * "subject" of the certificate used to sign the application.
     * <p>The format of the authority for X.509 certificates is defined
     * by the MIDP Printable Representation of X.509 Distinguished
     * Names as defined in class
     * <code>javax.microedition.pki.Certificate</code>. </p>
     *
     * @return the authority; may be <code>null</code>
     */
    public String getAuthority() {
        loadAppData();
        return authority;
    }

    /**
     * Initializes fields retrieved from AppProxy 'by-demand'.
     */
    private void loadAppData() {
        if (appname == null) {
            try {
                AppProxy app = 
                        AppProxy.getCurrent().forApp(storageId, classname);
                appname = app.getApplicationName();
                version = app.getVersion();
                authority = app.getAuthority();
            } catch (Throwable t) {
            }
            if (appname == null) {
                appname = "";
            }
        }
    }

    /**
     * Gets the n<sup>th</sup> ID of an application or content handler
     * allowed access to this content handler.
     * The ID returned for each index must be the equal to the ID
     * at the same index in the <tt>accessAllowed</tt> array passed to
     * {@link javax.microedition.content.Registry#register Registry.register}.
     *
     * @param index the index of the ID
     * @return the n<sup>th</sup> ID
     * @exception IndexOutOfBoundsException if index is less than zero or
     *     greater than or equal to the value of the
     *     {@link #accessAllowedCount accessAllowedCount} method.
     */
    public String getAccessAllowed(int index) {
        return get(index, getAccessRestricted());
    }

    /**
     * Gets the number of IDs allowed access by the content handler.
     * The number of IDs must be equal to the length of the array
     * of accessRestricted passed to
     * {@link javax.microedition.content.Registry#register Registry.register}.
     * If the number of IDs is zero then all applications and
     * content handlers are allowed access.
     *
     * @return the number of accessRestricteds
     */
    public int accessAllowedCount() {
        return getAccessRestricted().length;
    }

    /**
     * Determines if an ID MUST be allowed access by the content handler.
     * Access MUST be allowed if the ID has a prefix that exactly matches
     * any of the IDs returned by {@link #getAccessAllowed}.
     * The prefix comparison is equivalent to
     * <code>java.lang.String.startsWith</code>.
     *
     * @param ID the ID for which to check access
     * @return <code>true</code> if access MUST be allowed by the
     *  content handler;
     *  <code>false</code> otherwise
     * @exception NullPointerException if <code>accessRestricted</code>
     * is <code>null</code>
     */
    public boolean isAccessAllowed(String ID) {
        ID.length();                // check for null
        if (getAccessRestricted().length == 0) {
            return true;
        }
        for (int i = 0; i < getAccessRestricted().length; i++) {
            if (ID.startsWith(getAccessRestricted()[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get accesses for the content handler.
     *
     * @return array of allowed class names
     */
    private String [] getAccessRestricted() {
        if (accessRestricted == null) {
            accessRestricted =
                RegistryStore.getArrayField(ID, RegistryStore.FIELD_ACCESSES);
        }
        return accessRestricted;
    }

    /**
     * Gets the next Invocation request pending for this
     * ContentHandlerServer.
     * The method can be unblocked with a call to
     * {@link #cancelGetRequest cancelGetRequest}.
     * The application should process the Invocation as
     * a request to perform the <code>action</code> on the content.
     *
     * @param wait <code>true</code> if the method must wait for
     * for an Invocation if one is not available;
     * <code>false</code> if the method MUST NOT wait.
     * @param invocation an Invocation instance that will delegate to
     * the result; if any
     * @return the next pending Invocation or <code>null</code>
     *  if no Invocation is available; <code>null</code>
     *  if cancelled with {@link #cancelGetRequest cancelGetRequest}
     * @see javax.microedition.content.Registry#invoke
     * @see javax.microedition.content.ContentHandlerServer#finish
     */
    public InvocationImpl getRequest(boolean wait, Invocation invocation) {
        // Application has tried to get a request; reset cleanup flags on all
        if (requestCalls == 0) {
            InvocationStore.setCleanup(storageId, classname, false);
        }
        requestCalls++;

        InvocationImpl invoc =
            InvocationStore.getRequest(storageId, classname, wait);
        if (invoc != null) {
            // Keep track of number of requests delivered to the application
            AppProxy.requestForeground(invoc.invokingSuiteId,
                                       invoc.invokingClassname,
                                       invoc.suiteId,
                                       invoc.classname);
            invoc.invocation = invocation;
        }
        return invoc;
    }

    /**
     * Cancel a pending <code>getRequest</code>.
     * This method will force a Thread blocked in a call to the
     * <code>getRequest</code> method for the same application
     * context to return early.
     * If no Thread is blocked; this call has no effect.
     */
    public void cancelGetRequest() {
        InvocationStore.cancel();
    }

    /**
     * Finish this Invocation and set the status for the response.
     * The <code>finish</code> method may only be called when this
     * Invocation
     * has a status of <code>ACTIVE</code> or <code>HOLD</code>.
     * <p>
     * The content handler may modify the URL, type, action, or
     * arguments before invoking <code>finish</code>.
     * If the method {@link Invocation#getResponseRequired} returns
     * <code>true</code> then the modified
     * values MUST be returned to the invoking application.
     *
     * @param invoc the Invocation to finish
     * @param status the new status of the Invocation. This MUST be either
     *         <code>OK</code> or <code>CANCELLED</code>.
     *
     * @return <code>true</code> if the MIDlet suite MUST
     *   voluntarily exit before the response can be returned to the
     *   invoking application
     *
     * @exception IllegalArgumentException if the new
     *   <code>status</code> of the Invocation
     *    is not <code>OK</code> or <code>CANCELLED</code>
     * @exception IllegalStateException if the current
     *   <code>status</code> of the
     *   Invocation is not <code>ACTIVE</code> or <code>HOLD</code>
     * @exception NullPointerException if the invocation is <code>null</code>
     */
    protected boolean finish(InvocationImpl invoc, int status) {
        int currst = invoc.getStatus();
          if (currst != Invocation.ACTIVE && currst != Invocation.HOLD) {
             throw new IllegalStateException("Status already set");
         }
        // If ACTIVE or HOLD it must be an InvocationImpl
        return invoc.finish(status);
    }

    /**
     * Set the listener to be notified when a new request is
     * available for this content handler.  The request MUST
     * be retrieved using {@link #getRequest}.
     *
     * @param listener the listener to register;
     *   <code>null</code> to remove the listener.
     */
    public void setListener(RequestListener listener) {
        synchronized (this) {
            if (listener != null || listenerImpl != null) {
                // Create or update the active listener thread
                if (listenerImpl == null) {
                    listenerImpl =
                        new RequestListenerImpl(this, listener);
                } else {
                    listenerImpl.setListener(listener);
                }

                // If the listener thread no longer needed; clear it
                if (listener == null) {
                    listenerImpl = null;
                }
            }
        }
    }

    /**
     * Notify the request listener of a pending request.
     * Overridden by subclass.
     */
    protected void requestNotify() {
    }

    /**
     * Compare two ContentHandlerImpl's for equality.
     * Classname, storageID, and seqno must match.
     * @param other another ContentHandlerImpl
     * @return true if the other handler is for the same class,
     * storageID, and seqno.
     */
    boolean equals(ContentHandlerImpl other) {
        return seqno == other.seqno &&
            storageId == other.storageId &&
            classname.equals(other.classname);
    }

    /**
     * Debug routine to print the values.
     * @return a string with the details
     */
    public String toString() {
        if (AppProxy.LOG_INFO) {
            StringBuffer sb = new StringBuffer(80);
            sb.append("CH:");
            sb.append(" classname: ");
            sb.append(classname);
            sb.append(", removed: ");
            sb.append(removed);
            sb.append(", flag: ");
            sb.append(registrationMethod);
            sb.append(", types: ");
            toString(sb, types);
            sb.append(", ID: ");
            sb.append(ID);
            sb.append(", suffixes: ");
            toString(sb, suffixes);
            sb.append(", actions: ");
            toString(sb, actions);
            sb.append(", access: ");
            toString(sb, accessRestricted);
            sb.append(", suiteID: ");
            sb.append(storageId);
            sb.append(", authority: ");
            sb.append(authority);
            sb.append(", appname: ");
            sb.append(appname);
            return sb.toString();
        } else {
            return super.toString();
        }
    }

    /**
     * Append all of the strings inthe array to the string buffer.
     * @param sb a StringBuffer to append to
     * @param strings an array of strings.
     */
    private void toString(StringBuffer sb, String[] strings) {
        if (strings == null) {
            sb.append("null");
            return;
        }
        for (int i = 0; i < strings.length; i++) {
            if (i > 0) {
                sb.append(':');
            }
            sb.append(strings[i]);
        }
    }
}