FileDocCategorySizeDatePackage
Installer.javaAPI DocJ2ME MIDP 2.0132412Thu Nov 07 12:02:24 GMT 2002com.sun.midp.midletsuite

Installer.java

/*
 * @(#)Installer.java	1.113 02/10/14 @(#)
 *
 * Copyright (c) 2001-2002 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package com.sun.midp.midletsuite;

import java.util.Vector;
import java.util.Enumeration;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.DataInputStream;

import java.lang.String;
import java.lang.IllegalArgumentException;

import javax.microedition.io.Connector;
import javax.microedition.io.Connection;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.ConnectionNotFoundException;

import com.sun.midp.security.SecurityToken;
import com.sun.midp.security.Permissions;

import com.sun.midp.main.Configuration;

import com.sun.midp.midlet.Scheduler;
import com.sun.midp.midlet.MIDletSuite;
import com.sun.midp.midlet.MIDletInfo;

import com.sun.midp.io.Base64;
import com.sun.midp.io.Properties;
import com.sun.midp.io.HttpUrl;
import com.sun.midp.io.Util;

import com.sun.midp.io.j2me.push.PushRegistryImpl;

import com.sun.midp.io.j2me.storage.RandomAccessStream;
import com.sun.midp.io.j2me.storage.File;

import com.sun.midp.lcdui.Resource;

import com.sun.midp.rms.RecordStoreFile;

/**
 * An Installer manages MIDlet suites and libraries
 * present in a Java application environment.  An MIDlet suite
 * distributed as a descriptor and JAR pair.
 * The descriptor identifies the configuration and contains security
 * information and the manifest of the JAR describes the contents.
 * The implementation of an Installer is
 * specific to the platform and provides access to 
 * procedures that make an MIDlet suite visible to users. 
 * <P>
 * Each installed package is uniquely identified by a storage name
 * constructed from the combination
 * of the values of the <code>MIDlet-Name</code> and
 * <code>MIDlet-Vendor</code> attributes.
 * The syntax and content of the strings used to identify
 * installed packages are implementation dependent.
 * Only packages installed or upgraded using this API appear 
 * in the list of known packages.
 *
 */
public class Installer {
    /** Status code to signal connection to the JAD server was successful. */
    public static final int DOWNLOADING_JAD = 1;

    /** Status code to signal that another 1K of the JAD has been download. */
    public static final int DOWNLOADED_1K_OF_JAD = 2;

    /** Status code to signal connection to the JAR server was successful. */
    public static final int DOWNLOADING_JAR = 3;

    /** Status code to signal that another 1K of the JAR has been download. */
    public static final int DOWNLOADED_1K_OF_JAR = 4;

    /**
     * Status code to signal that download is done and the suite is being
     * verified.
     */
    public static final int VERIFYING_SUITE = 5;

    /**
     * Status code for local writing of the verified MIDlet suite.
     * Stopping the install at this point has no effect, so there user
     * should not be given a chance to stop the install.
     */
    public static final int STORING_SUITE = 6;

    /** Filename of Manifest inside the application archive. */
    public static final String JAR_MANIFEST       = "META-INF/MANIFEST.MF";

    /** MIDlet property for the size of the application data. */
    public static final String DATA_SIZE_PROP     = "MIDlet-Data-Size";

    /** MIDlet property for the size of the application archive. */
    public static final String JAR_SIZE_PROP      = "MIDlet-Jar-Size";

    /** MIDlet property for the application archive URL. */
    public static final String JAR_URL_PROP       = "MIDlet-Jar-URL";

    /** MIDlet property for the suite name. */
    public static final String SUITE_NAME_PROP    = "MIDlet-Name";

    /** MIDlet property for the suite vendor. */
    public static final String VENDOR_PROP        = "MIDlet-Vendor";

    /** MIDlet property for the suite version. */
    public static final String VERSION_PROP       = "MIDlet-Version";

    /** MIDlet property for the suite description. */
    public static final String DESC_PROP        = "MIDlet-Description";

    /** MIDlet property for the install notify URL. */
    public static final String NOTIFY_PROP        = "MIDlet-Install-Notify";

    /** MIDlet property for the delete notify URL. */
    public static final String DELETE_PROP        = "MIDlet-Delete-Notify";

    /** MIDlet property for the microedition configuration. */
    public static final String CONFIGURATION_PROP =
        "MicroEdition-Configuration";

    /** MIDlet property for the profile. */
    public static final String PROFILE_PROP       = "MicroEdition-Profile";

    /** MIDlet property for the required permissions. */
    public static final String PERMISSIONS_PROP     = "MIDlet-Permissions";

    /** MIDlet property for the optional permissions. */
    public static final String PERMISSIONS_OPT_PROP = "MIDlet-Permissions-Opt";

    /** Media-Type for valid application descriptor files. */
    public static final String JAD_MT = "text/vnd.sun.j2me.app-descriptor";

    /** Media-Type for valid Jar file. */
    public static final String JAR_MT_1 = "application/java";

    /** Media-Type for valid Jar file. */
    public static final String JAR_MT_2 = "application/java-archive";

    /** Max number of bytes to download at one time (1K). */
    private static final int MAX_DL_SIZE = 1024;

    /** Tag that signals that the HTTP server supports basic auth. */
    private static final String BASIC_TAG = "basic";

    /** Filename to save the domain owner of the suite. */
    static final String CA_FILENAME = "CA.utf";

    /** Filename to save the URL of the JAD. */
    static final String JAD_URL_FILENAME = "jadUrl.utf";

    /** Filename to save the URL of the JAR. */
    static final String JAR_URL_FILENAME = "jarUrl.utf";

    /** Filename to save the encoding of the JAD. */
    static final String JAD_ENCODING_FILENAME = "jadEncoding.utf";

    /** Filename to save the encoding of the JAD. */
    static final String SETTINGS_FILENAME = "settings.bin";

    /** Filename to suite JAR. */
    static final String JAR_FILENAME = "suite.jar";

    /** Filename to save the suite application descriptor. */
    static final String JAD_FILENAME = "suite.jad";

    /** Filename to save the delete notification URLs. */
    static final String DELETE_NOTIFY_FILENAME = "_delete_notify.utf";

    /**
     * Filename to save the JAR of the suite temporarily. This is used
     * to avoid overwriting an existing JAR prior to
     * verification.
     */
    static final String TMP_FILENAME      = "installer.tmp";

    /** Filename to save the application manifest. */
    static final String MANIFEST_FILENAME = "suite.mf";

    /** Filename of the list of installed suites. */
    static final String SUITE_LIST_FILENAME = "suites.utf";

    /** Success message for the suite provider. */
    static final String SUCCESS_MSG = "900 Success";

    /** Error message for the suite provider. */
    static final String INSUFFICIENT_MEM_MSG = "901 Insufficient Memory";

    /** Error message for the suite provider. */
    static final String USER_CANCELLED_MSG = "902 User Cancelled";

    /** Error message for the suite provider. */
    static final String JAR_SIZE_MISMATCH_MSG = "904 JAR Size Mismatch";

    /** Error message for the suite provider. */
    static final String ATTRIBUTE_MISMATCH_MSG = "905 Attribute Mismatch";

    /** Error message for the suite provider. */
    static final String INVALID_JAD_MSG = "906 Invalid Descriptor";

    /** Error message for the suite provider. */
    static final String INVALID_JAR_MSG = "907 Invalid JAR";

    /** Error message for the suite provider. */
    static final String INCOMPATIBLE_MSG =
        "908 Incompatible Configuration or Profile";

    /** Error message for authentication failure. */
    static final String AUTHENTICATION_FAILURE_MSG =
        "909 Application authentication failure";

    /** Error message for authorization failure. */
    static final String AUTHORIZATION_FAILURE_MSG =
        "910 Application authorization failure";

    /** Error message for push registration failure. */
    static final String PUSH_REG_FAILURE_MSG = 
        "911 Push registration failure";

    /** Error message for push registration failure. */
    static final String DELETE_NOTIFICATION_MSG = 
        "912 Deletion notification";

    /** Private reference to the singleton Installer class. */
    private static Installer myInstaller;

    /** This class has a different security domain than the MIDlet suite. */
    private static SecurityToken classSecurityToken;

    /** The unique storage name of the next MIDlet suite to run. */
    private String nextMidletSuiteToRun;

    /** The of the next MIDlet to run. */
    private String nextMidletToRun; 

    /** Cache of installed MIDlet suites. */
    private Vector midletSuiteList;

    /** Cache of delete notification URL list. */
    private Vector deleteNotifyURLList;

    /** HTTP connection to close when we stop the installation. */
    private HttpConnection httpConnection;

    /** HTTP stream to close when we stop the installation. */
    private InputStream httpInputStream;

    /** Holds the install state. */
    protected InstallStateImpl state;

    /** Holds the CLDC configuration string. */
    private String cldcConfig;

    /** Holds the MIDP supported profiles. */
    private Vector supportedProfiles;

    /** Use this to be the security domain for unsigned suites. */
    private String unsignedSecurityDomain = Permissions.UNTRUSTED_DOMAIN_NAME;

    /**
     * Initializes the security token for this class, so it can
     * perform actions that a normal MIDlet Suite cannot.
     *
     * @param token security token for this class.
     */
    public static void initSecurityToken(SecurityToken token) {
        if (classSecurityToken != null) {
            return;
        }

        classSecurityToken = token;
        MIDletSuiteImpl.initSecurityToken(classSecurityToken);
    }

    /**
     * Saves any of the settings (security or others) that the user may have
     * changed.
     *
     * @param token security token with internal MIDP permission
     * @param suiteStorageRoot storage root of the suite
     * @param pushInterruptSetting push interrupt setting
     * @param permissions security permissions for the suite
     * @param trusted true if the suite is trusted
     *
     * @exception IOException if an error happens while writing
     */
    public static void saveSuiteSettings(SecurityToken token,
            String suiteStorageRoot, byte pushInterruptSetting,
            byte[][] permissions, boolean trusted) throws IOException {
        saveSuiteSettings(new RandomAccessStream(token),
            suiteStorageRoot, pushInterruptSetting, permissions, trusted);
    }

    /**
     * Saves any of the settings (security or others) that the user may have
     * changed.
     *
     * @param suiteStorageRoot storage root of the suite
     * @param pushInterruptSetting push interrupt setting
     * @param permissions security permissions for the suite
     * @param trusted true if the suite is trusted
     *
     * @exception IOException if an error happens while writing
     */
    public static void saveSuiteSettings(String suiteStorageRoot,
            byte pushInterruptSetting, byte[][] permissions, boolean trusted)
            throws IOException {
        saveSuiteSettings(new RandomAccessStream(), suiteStorageRoot,
            pushInterruptSetting, permissions, trusted);
    }

    /**
     * Saves any of the settings (security or others) that the user may have
     * changed.
     *
     * @param storage pre-allocated storage object, but not connected
     * @param suiteStorageRoot storage root of the suite
     * @param pushInterruptSetting push interrupt setting
     * @param permissions security permissions for the suite
     * @param trusted true if the suite is trusted
     *
     * @exception IOException if an error happens while writing
     */
    private static void saveSuiteSettings(RandomAccessStream storage,
            String suiteStorageRoot, byte pushInterruptSetting,
            byte[][] permissions, boolean trusted) throws IOException {
        DataOutputStream storageStream;
        byte[] maximums = permissions[Permissions.MAX_LEVELS];
        byte[] currentLevels = permissions[Permissions.CUR_LEVELS];

        storage.connect(suiteStorageRoot + SETTINGS_FILENAME,
                        RandomAccessStream.READ_WRITE_TRUNCATE);
        try {
            storageStream = storage.openDataOutputStream();

            /*
             * write the version of the file
             * Note that version number only has to
             * increase if data has been removed, not if new data has been
             * added to the end of the file.
             */
            storageStream.writeByte(1);

            storageStream.writeBoolean(trusted);

            storageStream.writeByte(pushInterruptSetting);

            storageStream.writeByte(currentLevels.length);
            for (int i = 0; i < currentLevels.length; i++) {
                storageStream.writeByte(currentLevels[i]);
            }

            storageStream.writeByte(maximums.length);
            for (int i = 0; i < maximums.length; i++) {
                storageStream.writeByte(maximums[i]);
            }

            // Add new items at the end of the file
        } finally {
            try {
                storage.disconnect();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    /**
     * Returns a reference to the singleton Installer object.
     *
     * @return the installer reference
     *
     * @exception SecurityException if the caller does not have permission
     *   to install software
     */
    public static Installer getInstaller() throws SecurityException {
        MIDletSuite midletSuite = Scheduler.getScheduler().getMIDletSuite();

        // if a MIDlet suite is not scheduled, assume the JAM is calling.
        if (midletSuite != null) {
            midletSuite.checkIfPermissionAllowed(Permissions.AMS);
        }

        return getInstallerCommon();
    }

    /**
     * Returns a reference to the singleton Installer object.
     *
     * @param securityToken security token of the calling class
     *
     * @return the installer reference
     *
     * @exception SecurityException if the caller does not have permission
     *   to install software
     */
    public static Installer getInstaller(SecurityToken securityToken)
           throws SecurityException {
        securityToken.checkIfPermissionAllowed(Permissions.AMS);

        return getInstallerCommon();
    }

    /**
     * Returns a reference to the singleton Installer object.
     *
     * @return the installer reference
     */
    private static Installer getInstallerCommon() {
        String nameOfInstaller;

        if (myInstaller != null) {
            return myInstaller;
        }

        /*
         * In internal.config an alternate installer, such as a
         * SecureInstaller can be specified.
         */
        nameOfInstaller = Configuration.getProperty(
                              "com.sun.midp.midletsuite.installer");
        if (nameOfInstaller == null) {
            myInstaller = new SecureInstaller();
            return myInstaller;
        }
        
        try {
            myInstaller = 
                (Installer)Class.forName(nameOfInstaller).newInstance();
            return myInstaller;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * Returns a storage identifier of an application.
     *
     * @param vendorName name of the vendor that created the application, as
     *          given in a JAD file
     * @param appName name of the application, as given in a JAD file
     *
     * @return the platform-specific storage name of the application
     *          given by vendorName and appName
     */
    public static String makeStorageName(String vendorName, String appName) {
        return File.unicodeToAsciiFilename(vendorName) + '_' +
            File.unicodeToAsciiFilename(appName) + '_';
    }

    /**
     * Constructor to prevent a second instantiation of the Installer.
     */
    protected Installer() {
        if (myInstaller != null) {
            throw new
                SecurityException("Illegal instantiation of the Installer");
        }
    }

    /**
     * Installs a software package from the given URL. The URL is assumed
     * refer to an application descriptor.
     * <p>
     * If the component to be installed is the same as an existing
     * component (by comparing the <code>MIDlet-Name</code>,
     * <code>MIDlet-Vendor</code> attributes)
     * then this install is an upgrade if the version number is greater 
     * than the current version.  If so, the new version replaces in its
     * entirety the current version.
     * <p>
     * It is implementation dependent when the upgraded component is
     * made available for use.
     * <p>
     * The implementation of install must be robust in the presence
     * of failures such as running out of memory.  If this method
     * throws an exception then the package must <em>not</em> be installed
     * and any previous version of the component must be left intact
     * and operational.
     * <p>
     * To receive status updates and installer warnings, provide an install
     * listener. If no listener is provided all warnings will be thrown
     * as exceptions.
     *
     * @param location the URL from which the application descriptor can be
     *        updated
     * @param force if <code>true</code> the MIDlet suite components to be
     *              installed will overwrite any existing components without
     *              any version comparison
     * @param removeRMS if <code>true</code> and existing RMS data will be
     *              removed when overwriting an existing suite
     * @param installListener object to receive status updates and install
     *     warnings, can be null
     *
     * @return the unique name of the installed package. 
     *
     * @exception ConnectionNotFoundException if JAD URL is invalid
     * @exception IOException is thrown if any error prevents the installation
     *   of the MIDlet suite, including being unable to access the application
     *   descriptor or JAR
     * @exception InvalidJadException if the downloaded application descriptor
     *   is invalid
     * @exception SecurityException if the caller does not have permission
     *   to install software
     * @exception IllegalArgumentException is thrown, if the location of the
     * descriptor file is not specified
     */
    public String installJad(String location, boolean force, boolean removeRMS,
            InstallListener installListener)
            throws IOException, InvalidJadException, SecurityException {

        state = new InstallStateImpl();

        state.jadUrl = location;
        state.force = force;
        state.removeRMS = removeRMS;
        state.nextStep = 1;
        state.listener = installListener;

        return performInstall();
    }

    /**
     * Installs a software package from the given URL. The URL is assumed
     * refer to a JAR.
     * <p>
     * If the component to be installed is the same as an existing
     * component (by comparing the <code>MIDlet-Name</code>,
     * <code>MIDlet-Vendor</code> attributes)
     * then this install is an upgrade if the version number is greater 
     * than the current version.  If so, the new version replaces in its
     * entirety the current version.
     * <p>
     * It is implementation dependent when the upgraded component is
     * made available for use.
     * <p>
     * The implementation of install must be robust in the presence
     * of failures such as running out of memory.  If this method
     * throws an exception then the package must <em>not</em> be installed
     * and any previous version of the component must be left intact
     * and operational.
     * <p>
     * To receive status updates and installer warnings, provide an install
     * listener. If no listener is provided all warnings will be thrown
     * as exceptions.
     *
     * @param location the URL from which the JAR can be updated
     * @param force if <code>true</code> the MIDlet suite components to be
     *              installed will overwrite any existing components without
     *              any version comparison
     * @param removeRMS if <code>true</code> and existing RMS data will be
     *              removed when overwriting an existing suite
     * @param installListener object to receive status updates and install
     *     warnings, can be null
     *
     * @return the unique name of the installed package. 
     *
     * @exception IOException is thrown if any error prevents the installation
     *   of the MIDlet suite, including being unable to access the JAR
     * @exception InvalidJadException if the downloaded JAR is invalid
     * @exception SecurityException if the caller does not have permission
     *   to install software
     * @exception IllegalArgumentException is thrown, if the location of the
     * JAR specified
     */
    public String installJar(String location, boolean force, boolean removeRMS,
            InstallListener installListener)
            throws IOException, InvalidJadException, SecurityException {

        if (location == null || location.length() == 0) {
            throw
                new IllegalArgumentException("Must specify URL of .jar file");
        }

        state = new InstallStateImpl();

        state.jarUrl = location;
        state.force = force;
        state.removeRMS = removeRMS;
        state.nextStep = 4;
        state.listener = installListener;

        return performInstall();
    }

    /**
     * Performs an install.
     *
     * @return the unique name of the installed package
     *
     * @exception IOException is thrown, if an I/O error occurs during
     * descriptor or jar file download
     * @exception InvalidJadException is thrown, if the descriptor file is not 
     * properly formatted or does not contain the required 
     * information
     * @exception IllegalArgumentException is thrown, if the
     * descriptor file is not specified
     */
    private synchronized String performInstall()
            throws IOException, InvalidJadException {

	/* Disable push interruptions during install. */
	PushRegistryImpl.enablePushLaunch(classSecurityToken, false);

	try {
	    state.startTime = System.currentTimeMillis();
	    
	    while (state.nextStep < 7) {
		/*
		 * clear the previous warning, so we can tell if another has
		 * happened
		 */
		state.exception = null;
		
		if (state.stopInstallation) {
		    postInstallMsgBackToProvider(USER_CANCELLED_MSG);
		    throw new IOException("stopped");
		}
		
		switch (state.nextStep) {
		case 1:
		    installStep1();
		    break;
		    
		case 2:
		    installStep2();
		    break;
		    
		case 3:
		    installStep3();
		    break;
		    
		case 4:
		    installStep4();
		    break;
		    
		case 5:
		    installStep5();
		    break;
		    
		case 6:
		    installStep6();
		    break;
		}
		
		if (state.exception != null) {
		    if (state.listener == null) {
			throw state.exception;
		    }
		    
		    if (!state.listener.warnUser(state)) {
			state.stopInstallation = true;
			postInstallMsgBackToProvider(USER_CANCELLED_MSG);
			throw state.exception;
		    }
		}
	    }
	} finally {
	    PushRegistryImpl.enablePushLaunch(classSecurityToken, true);
	}

        return state.storageName;
    }

    /**
     * Downloads the JAD, save it in the install state.
     * Parse the JAD, make sure it has
     * the required properties, and save them in the install state.
     *
     * @exception IOException is thrown, if an I/O error occurs during
     * descriptor or jar file download
     * @exception InvalidJadException is thrown, if the descriptor file is not 
     * properly formatted or does not contain the required attributes
     * @exception IllegalArgumentException is thrown, if the
     * descriptor file is not specified
     */
    private void installStep1() throws IOException, InvalidJadException {
        if (state.jadUrl == null || state.jadUrl.length() == 0) {
            throw
                new IllegalArgumentException("Must specify URL of .jad file");
        }

        state.jad = downloadJAD();
        if (state.exception != null) {
            return;
        }

        state.jadProps = new JadProperties();
        try {
            state.jadProps.load(new ByteArrayInputStream(state.jad),
                state.jadEncoding);
        } catch (InvalidJadException ije) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw ije;
        }

        state.suiteName = state.jadProps.getProperty(SUITE_NAME_PROP);
        if (state.suiteName == null || state.suiteName.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_SUITE_NAME);
        }

        state.vendor = state.jadProps.getProperty(VENDOR_PROP);
        if (state.vendor == null || state.vendor.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_VENDOR);
        }

        state.version = state.jadProps.getProperty(VERSION_PROP);
        if (state.version == null || state.version.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_VERSION);
        }

        state.storageName = makeStorageName(state.vendor, state.suiteName);

        checkPreviousVersion();
        state.nextStep++;
    }

    /**
     * If the JAD belongs to an installed suite, check the URL against the
     * installed one.
     */
    private void installStep2() {
        state.nextStep++;

        if (state.isPreviousVersion) {
            checkForDifferentDomains(state.jadUrl);
        }
    }

    /**
     * Makes sure the suite can fit in storage and confirm installation
     * with the user.
     *
     * @exception IOException is thrown, if an I/O error occurs during
     * descriptor or jar file download
     * @exception InvalidJadException is thrown, if the descriptor file is not 
     * properly formatted or does not contain the required 
     */
    private void installStep3()
            throws IOException, InvalidJadException {
        String sizeString;
        int dataSize;
        int suiteSize;

        sizeString = state.jadProps.getProperty(JAR_SIZE_PROP);
        if (sizeString == null) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_JAR_SIZE);
        }
        
        try {
            state.expectedJarSize = Integer.parseInt(sizeString);
        } catch (NumberFormatException e) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.INVALID_VALUE);
        }

        sizeString = state.jadProps.getProperty(DATA_SIZE_PROP);
        if (sizeString == null) {
            dataSize = 0;
        } else {
            try {
                dataSize = Integer.parseInt(sizeString);
            } catch (NumberFormatException e) {
                postInstallMsgBackToProvider(INVALID_JAD_MSG);
                throw new
                    InvalidJadException(InvalidJadException.INVALID_VALUE);
            }
        }

        /*
         * A suite is a jad + jar + manifest + url + data size.
         * lets say the manifest is the same size as the jad
         * since we do know at this point. the size is in bytes,
         * UTF-8 chars can be upto 3 bytes
         */
        suiteSize = state.expectedJarSize + (state.jad.length * 2) +
                    (state.jadUrl.length() * 3) + dataSize;
        
        state.file = new File(classSecurityToken);

        if (suiteSize > state.file.getBytesAvailableForFiles()) {
            postInstallMsgBackToProvider(INSUFFICIENT_MEM_MSG);

            // the size reported to the user should be in K and rounded up
            throw new
                InvalidJadException(InvalidJadException.INSUFFICIENT_STORAGE,
                    Integer.toString((suiteSize + 1023)/ 1024));
        }

        state.jarUrl = state.jadProps.getProperty(JAR_URL_PROP);
        if (state.jarUrl == null || state.jarUrl.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_JAR_URL);
        }

        if (state.listener != null &&
                !state.listener.confirmJarDownload(state)) {
            state.stopInstallation = true;
            postInstallMsgBackToProvider(USER_CANCELLED_MSG);
            throw new IOException("stopped");
        }

        state.nextStep++;
    }

    /**
     * Downloads the JAR, make sure it is the correct size, make sure
     * the required attributes match the JAD's. Then store the
     * application.
     *
     * @exception IOException is thrown, if an I/O error occurs during
     * descriptor or jar file download
     * @exception InvalidJadException is thrown, if the descriptor file is not 
     * properly formatted or does not contain the required 
     */
    private void installStep4()
            throws IOException, InvalidJadException {
        int bytesDownloaded;
        MIDletInfo midletInfo;
        String midlet;
        String classfilename;


        // Save jar file to temp name; we need to do this to read the
        // manifest entry, but, don't want to overwrite an exisiting
        // application in case there are problems with the manifest
        state.storageRoot = File.getStorageRoot();
        state.tempFilename = state.storageRoot + TMP_FILENAME;

        bytesDownloaded = downloadJAR(state.tempFilename);

        if (state.exception != null) {
            return;
        }

        if (state.listener != null) {
            state.listener.updateStatus(VERIFYING_SUITE, state);
        }

        try {
            state.storage = new RandomAccessStream(classSecurityToken);

            verifyJar(state.storage, state.tempFilename);

            // Create JAR Properties (From .jar file's MANIFEST
            try {
                state.manifest = JarReader.readJarEntry(classSecurityToken,
                                     state.tempFilename, JAR_MANIFEST);
                if (state.manifest == null) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new
                        InvalidJadException(InvalidJadException.CORRUPT_JAR,
                                            JAR_MANIFEST);
                }
            } catch (IOException ioe) {
                postInstallMsgBackToProvider(INVALID_JAR_MSG);
                throw new
                    InvalidJadException(InvalidJadException.CORRUPT_JAR,
                                        JAR_MANIFEST);
            }

            state.jarProps = new ManifestProperties();

            try {
                state.jarProps.load(new ByteArrayInputStream(state.manifest));
            } catch (InvalidJadException ije) {
                postInstallMsgBackToProvider(INVALID_JAR_MSG);
                throw ije;
            }

            for (int i = 1; ; i++) {
                midlet = state.getAppProperty("MIDlet-" + i);
                if (midlet == null) {
                    break;
                }
		    
                midletInfo = new MIDletInfo(midlet);
                classfilename = midletInfo.classname.replace('.', '/') +
                                ".class";

                try {
		    /* Attempt to read the MIDlet from the JAR file. */
		    if (JarReader.readJarEntry(classSecurityToken,
                            state.tempFilename, classfilename) == null) {
                        postInstallMsgBackToProvider(INVALID_JAR_MSG);
                        throw new InvalidJadException(
                            InvalidJadException.CORRUPT_JAR, classfilename);
                    }
                } catch (IOException ioe) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new
                        InvalidJadException(InvalidJadException.CORRUPT_JAR,
                                            classfilename);
                }
            }

            // move on to the next step after a warning
            state.nextStep++;

            // Check Manifest entries against .jad file
            if (state.jadUrl != null) {
                if (bytesDownloaded != state.expectedJarSize) {
                    postInstallMsgBackToProvider(JAR_SIZE_MISMATCH_MSG);
                    throw new  InvalidJadException(
                        InvalidJadException.JAR_SIZE_MISMATCH);
                }

                if (!state.suiteName.equals(
                        state.jarProps.getProperty(SUITE_NAME_PROP))) {
                    postInstallMsgBackToProvider(ATTRIBUTE_MISMATCH_MSG);
                    throw new InvalidJadException(
                        InvalidJadException.SUITE_NAME_MISMATCH);
                }

                if (!state.version.equals(
                        state.jarProps.getProperty(VERSION_PROP))) {
                    postInstallMsgBackToProvider(ATTRIBUTE_MISMATCH_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.VERSION_MISMATCH);
                }

                if (!state.vendor.equals(
                        state.jarProps.getProperty(VENDOR_PROP))) {
                    postInstallMsgBackToProvider(ATTRIBUTE_MISMATCH_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.VENDOR_MISMATCH);
                }
            } else {
                state.file = new File(classSecurityToken);

                state.suiteName = state.jarProps.getProperty(SUITE_NAME_PROP);
                if (state.suiteName == null ||
                        state.suiteName.length() == 0) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.MISSING_SUITE_NAME);
                }

                state.vendor = state.jarProps.getProperty(VENDOR_PROP);
                if (state.vendor == null || state.vendor.length() == 0) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.MISSING_VENDOR);
                }

                state.version = state.jarProps.getProperty(VERSION_PROP);
                if (state.version == null || state.version.length() == 0) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.MISSING_VERSION);
                }
                
                try {
                    checkVersionFormat(state.version);
                } catch (NumberFormatException nfe) {
                    postInstallMsgBackToProvider(INVALID_JAR_MSG);
                    throw new InvalidJadException(
                         InvalidJadException.INVALID_VERSION);
                }

                // NYI if already installed, check the domain of the JAR URL

                state.storageName = makeStorageName(state.vendor,
                                                    state.suiteName);

                checkPreviousVersion();
            }
        } catch (Exception e) {
            state.file.delete(state.tempFilename);

            if (e instanceof IOException) {
                throw (IOException)e;
            }

            throw (RuntimeException)e;
        }
    }

    /**
     * If the JAR belongs to an installed suite if there was
     * no JAD, check the URL against the installed one.
     */
    private void installStep5() {
        state.nextStep++;

        if (state.jadUrl == null && state.isPreviousVersion) {
            checkForDifferentDomains(state.jarUrl);
        }
    }

    /**
     * Checks the permissions and store the suite.
     *
     * @exception IOException is thrown, if an I/O error occurs during
     * storing the suite
     * @exception InvalidJadException is thrown, if the there is
     * permission problem
     */
    private void installStep6() throws IOException, InvalidJadException {
        String suiteStorageRoot;
        DataOutputStream storageStream;
        String securityDomain;

        try {            
            if (state.ca != null) {
                // suite was signed
                securityDomain = getSecurityDomainName(state.storageRoot,
                                                       state.ca);
            } else {
                securityDomain = unsignedSecurityDomain;
            }

            /*
             * untrusted and test domains do not check for requested
             * permissions
             */
            if (Permissions.UNTRUSTED_DOMAIN_NAME.equals(securityDomain)) {
                if (state.isPreviousVersion &&
                        state.previousSuite.isTrusted()) {
                    // Do not overwrite trusted suites with untrusted ones
                    postInstallMsgBackToProvider(AUTHORIZATION_FAILURE_MSG);
                    throw new InvalidJadException(
                        InvalidJadException.TRUSTED_OVERWRITE_FAILURE,
                        state.previousCA);
                }                    

                state.permissions = Permissions.forDomain(classSecurityToken,
                                                          securityDomain);

                /*
                 * To keep public key management simple, there is only one
                 * trusted keystore. So it is possible that the CA for
                 * the suite is untrusted. This may be done on purpose for
                 * testing. This is OK, but do not confuse the user by saying
                 * the untrusted suite is authorized, so set the CA name to
                 * null.
                 */
                state.ca = null;
            } else {
                /*
                 * For trusted suites, make sure an properties duplicated in
                 * both the manifest and JAD are the same.
                 */
                if (state.jadUrl != null) {
                    checkForJadManifestMismatches();
                }

                state.trusted = true;
                state.permissions = getInitialPermissions(securityDomain);
            }

            if (state.isPreviousVersion) {
                applyCurrentUserLevelPermissions(
                  state.previousSuite.getPermissions()[Permissions.CUR_LEVELS],
                    state.permissions[Permissions.MAX_LEVELS],
                    state.permissions[Permissions.CUR_LEVELS]);

                if (state.removeRMS) {
                    // override suite comparisons, just remove RMS
                    RecordStoreFile.removeRecordStoresForSuite(
                        classSecurityToken,
                        state.storageRoot + state.storageName);
                } else {
                    processPreviousRMS();
                }
            }

            state.securityToken = new SecurityToken(classSecurityToken,
                                                    state.permissions);

            checkConfiguration();
            matchProfile();

            suiteStorageRoot = state.storageRoot + state.storageName;

            // make sure at least 1 second has passed
            while (System.currentTimeMillis() - state.startTime < 1000);

            synchronized (state) {
                // this is the point of no return, one last check
                if (state.stopInstallation) {
                    postInstallMsgBackToProvider(USER_CANCELLED_MSG);
                    throw new IOException("stopped");
                }

                if (state.listener != null) {
                    state.listener.updateStatus(STORING_SUITE, state);
                }

                state.writingSuite = true;
            }

            registerPushConnections();

        } catch (Exception e) {
            state.file.delete(state.tempFilename);
            if (e instanceof IOException) {
                throw (IOException)e;
            }

            throw (RuntimeException)e;
        }

        try {
            state.file.rename(state.tempFilename,
                              suiteStorageRoot + JAR_FILENAME);
            
            state.storage.connect(suiteStorageRoot + MANIFEST_FILENAME,
                              RandomAccessStream.READ_WRITE_TRUNCATE);
            state.storage.writeBytes(state.manifest, 0, state.manifest.length);
            state.storage.disconnect();

            if (state.file.exists(suiteStorageRoot + JAD_URL_FILENAME)) {
                // erase the old JAD URL since we did not get new one
                state.file.delete(suiteStorageRoot + JAD_URL_FILENAME);
                state.file.delete(suiteStorageRoot + JAD_FILENAME);
            }

            if (state.jadUrl != null) {
                // Save JAD, JAR & manifest files
                state.storage.connect(suiteStorageRoot + JAD_FILENAME, 
                                  RandomAccessStream.READ_WRITE_TRUNCATE);
                state.storage.writeBytes(state.jad, 0, state.jad.length);
                state.storage.disconnect();

                // convert the JAD URL to UTF8 and write it to storage
                state.storage.connect(suiteStorageRoot + JAD_URL_FILENAME,
                                  RandomAccessStream.READ_WRITE_TRUNCATE);
                storageStream = state.storage.openDataOutputStream();
                storageStream.writeUTF(state.jadUrl);
                storageStream.close();
                state.storage.disconnect();
            }

            // convert the JAR URL to UTF8 and write it to storage
            state.storage.connect(suiteStorageRoot + JAR_URL_FILENAME,
                                  RandomAccessStream.READ_WRITE_TRUNCATE);
            storageStream = state.storage.openDataOutputStream();
            storageStream.writeUTF(state.jarUrl);
            storageStream.close();
            state.storage.disconnect();

            if (state.jadEncoding != null) {
                // convert the JAD encoding to UTF8 and write it to storage
                state.storage.connect(suiteStorageRoot + JAD_ENCODING_FILENAME,
                                  RandomAccessStream.READ_WRITE_TRUNCATE);
                storageStream = state.storage.openDataOutputStream();
                storageStream.writeUTF(state.jadEncoding);
                storageStream.close();
                state.storage.disconnect();
            } else if (state.file.exists(suiteStorageRoot +
                                         JAD_ENCODING_FILENAME)) {
                // erase the old encoding since we did not get new one
                state.file.delete(suiteStorageRoot + JAD_ENCODING_FILENAME);
            }

            // todo get the jad encoding and write to a file
            if (state.ca != null) {
                // convert the domain owner to UTF8 and write it to storage
                state.storage.connect(suiteStorageRoot + CA_FILENAME,
                                  RandomAccessStream.READ_WRITE_TRUNCATE);
                storageStream = state.storage.openDataOutputStream();
                storageStream.writeUTF(state.ca);
                storageStream.close();
                state.storage.disconnect();
            }

            saveSuiteSettings(classSecurityToken, suiteStorageRoot,
                state.pushInterruptSetting, state.permissions, state.trusted);

            addToSuiteList(state.storageName);

            state.nextStep++;

            try {
                postInstallMsgBackToProvider(SUCCESS_MSG);
            } catch (RuntimeException re) {
                /*
                 * The suite is successfully installed, but the post of the
                 * status message failed. Do not let this failure prevent 
                 * the suite from being used.
                 */
            }

            return;
        } catch (Exception e) {
            try {
                state.storage.disconnect();
            } catch (IOException ex) {
                // ignore
            }

            cleanUp(state.storageName);

            if (e instanceof IOException) {
                throw (IOException)e;
            }

            throw (RuntimeException)e;
        }
    }

    /**
     * Downloads an application descriptor file from the given URL.
     *
     * @return a byte array representation of the file or null if not found
     *
     * @exception IOException is thrown if any error prevents the download
     *   of the JAD
     */
    private byte[] downloadJAD() throws IOException {
        String[] encoding = new String[1];
        ByteArrayOutputStream bos = new ByteArrayOutputStream(MAX_DL_SIZE);
        String[] acceptableTypes = {JAD_MT};
        String[] extraFieldKeys = new String[3];
        String[] extraFieldValues = new String[3];
        String locale;
        String prof = System.getProperty("microedition.profiles");
        int space = prof.indexOf(' ');
        if (space != -1) {
            prof = prof.substring(0, space);
        }


        extraFieldKeys[0] = "User-Agent";
        extraFieldValues[0] = "Profile/" + prof
                             + " Configuration/" +
                             System.getProperty("microedition.configuration");

        extraFieldKeys[1] = "Accept-Charset";
        extraFieldValues[1] = "UTF-8, " +
                              System.getProperty("microedition.encoding");

        /* locale can be null */
        locale = System.getProperty("microedition.locale");
        if (locale != null) {
            extraFieldKeys[2] = "Accept-Language";
            extraFieldValues[2] = locale;
        }

        state.beginTransferDataStatus = DOWNLOADING_JAD;
        state.transferStatus = DOWNLOADED_1K_OF_JAD;

        /*
         * Do not send the list of acceptable types because some servers
         * will send a 406 if the URL is to a JAR. It is better to
         * reject the resource at the client after check the media-type so
         * if the type is JAR a JAR only install can be performed.
         */
        downloadResource(state.jadUrl, extraFieldKeys, extraFieldValues,
                         acceptableTypes, false, false, bos, encoding,
                         InvalidJadException.INVALID_JAD_URL,
                         InvalidJadException.JAD_SERVER_NOT_FOUND,
                         InvalidJadException.JAD_NOT_FOUND,
                         InvalidJadException.INVALID_JAD_TYPE);

        state.jadEncoding = encoding[0];
        return bos.toByteArray();
    }

    /**
     * Downloads an application archive file from the given URL into the
     * given file. Automatically handle re-trys.
     *
     * @param filename name of the file to write. This file resides
     *          in the storage area of the given application
     *
     * @return size of the JAR
     *
     * @exception IOException is thrown if any error prevents the download
     *   of the JAR
     */
    private int downloadJAR(String filename)
            throws IOException {
        HttpUrl parsedUrl;
        String url;
        String[] acceptableTypes = {JAR_MT_1, JAR_MT_2};
        int jarSize;

        RandomAccessStream jarOutputStream = null;
        OutputStream outputStream = null;

        parsedUrl = new HttpUrl(state.jarUrl);
        if (parsedUrl.authority == null && state.jadUrl != null) {
            // relative URL, add the JAD URL as the base
            try {
                parsedUrl.addBaseUrl(state.jadUrl);
            } catch (IOException e) {
                postInstallMsgBackToProvider(INVALID_JAD_MSG);
                throw new InvalidJadException(
                         InvalidJadException.INVALID_JAR_URL);
            }

            url = parsedUrl.toString();

            // The JAR URL saved to storage MUST be absolute
            state.jarUrl = url;
        } else {
            url = state.jarUrl;
        }

        jarOutputStream = new RandomAccessStream(classSecurityToken);
        jarOutputStream.connect(filename,
                                RandomAccessStream.READ_WRITE_TRUNCATE);
        outputStream = jarOutputStream.openOutputStream();

        try {
            state.beginTransferDataStatus = DOWNLOADING_JAR;
            state.transferStatus = DOWNLOADED_1K_OF_JAR;
            jarSize = downloadResource(url, null, null,
                         acceptableTypes, true, true, outputStream, null,
                         InvalidJadException.INVALID_JAR_URL,
                         InvalidJadException.JAR_SERVER_NOT_FOUND,
                         InvalidJadException.JAR_NOT_FOUND,
                         InvalidJadException.INVALID_JAR_TYPE);
            return jarSize;
        } catch (InvalidJadException ije) {
            switch (ije.getReason()) {
            case InvalidJadException.JAR_SERVER_NOT_FOUND:
            case InvalidJadException.JAR_NOT_FOUND:
                postInstallMsgBackToProvider(INVALID_JAD_MSG);
                break;

            case InvalidJadException.INVALID_JAR_TYPE:
                postInstallMsgBackToProvider(INVALID_JAR_MSG);
            }

            throw ije;
        } finally {
            try {
                jarOutputStream.disconnect();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * Downloads an resource from the given URL into the output stream.
     *
     * @param url location of the resource to download
     * @param extraFieldKeys keys to the extra fields to put in the request
     * @param extraFieldValues values to the extra fields to put in the request
     * @param acceptableTypes list of acceptable media types for this resource,
     *                        there must be at least one
     * @param sendAcceptableTypes if true the list of acceptable media types
     *       for this resource will be sent in the request
     * @param allowNoMediaType if true it is not consider an error if
     *       the media type is not in the response
     *       for this resource will be sent in the request
     * @param output output stream to write the resource to
     * @param encoding an array to receive the character encoding of resource,
     *                 can be null
     * @param invalidURLCode reason code to use when the URL is invalid
     * @param serverNotFoundCode reason code to use when the server is not
     *     found
     * @param resourceNotFoundCode reason code to use when the resource is not
     *     found on the server
     * @param invalidMediaTypeCode reason code to use when the media type of
     *     the resource is not valid
     *
     * @return size of the resource
     *
     * @exception IOException is thrown if any error prevents the download
     *   of the resource
     */
    private int downloadResource(String url, String[] extraFieldKeys,
            String[] extraFieldValues, String[] acceptableTypes,
            boolean sendAcceptableTypes, boolean allowNoMediaType,
            OutputStream output, String[] encoding, int invalidURLCode,
            int serverNotFoundCode, int resourceNotFoundCode,
            int invalidMediaTypeCode) throws IOException {
        Connection conn = null;
        StringBuffer acceptField;
        int responseCode;
        String retryAfterField;
        int retryInterval;
        String mediaType;

        try {
            for (; ; ) {
                try {
                    conn = Connector.open(url, Connector.READ);
                } catch (IllegalArgumentException e) {
                    throw new InvalidJadException(invalidURLCode, url);
                } catch (ConnectionNotFoundException e) {
                    // protcol not found
                    throw new InvalidJadException(invalidURLCode, url);
                }

                if (!(conn instanceof HttpConnection)) {
                    // only HTTP or HTTPS are supported
                    throw new InvalidJadException(invalidURLCode, url);
                }

                httpConnection = (HttpConnection)conn;

                if (extraFieldKeys != null) { 
                    for (int i = 0; i < extraFieldKeys.length &&
                            extraFieldKeys[i] != null; i++) {
                        httpConnection.setRequestProperty(extraFieldKeys[i],
                                                          extraFieldValues[i]);
                    }
                }
                    
                // 256 is given to avoid resizing without adding lengths
                acceptField = new StringBuffer(256);

                if (sendAcceptableTypes) {
                    // there must be one or more acceptable media types
                    acceptField.append(acceptableTypes[0]);
                    for (int i = 1; i < acceptableTypes.length; i++) {
                        acceptField.append(", ");
                        acceptField.append(acceptableTypes[i]);
                    }

                    httpConnection.setRequestProperty("Accept",
                                                      acceptField.toString());
                }

                httpConnection.setRequestMethod(HttpConnection.GET);

                if (state.username != null &&
                        state.password != null) {
                    httpConnection.setRequestProperty("Authorization",
                        formatAuthCredentials(state.username,
                                              state.password));
                }

                if (state.proxyUsername != null &&
                        state.proxyPassword != null) {
                    httpConnection.setRequestProperty("Proxy-Authorization",
                        formatAuthCredentials(state.proxyUsername,
                                              state.proxyPassword));
                }

                try {
                    responseCode = httpConnection.getResponseCode();
                } catch (IOException ioe) {
                    if (httpConnection.getHost() == null) {
                        throw new InvalidJadException(invalidURLCode, url);
                    }

                    throw new InvalidJadException(serverNotFoundCode, url);
                }

                if (responseCode != HttpConnection.HTTP_UNAVAILABLE) {
                    break;
                }

                retryAfterField = httpConnection.getHeaderField("Retry-After");
                if (retryAfterField == null) {
                    break;
                }

                try {
                    /*
                     * see if the retry interval is in seconds, and
                     * not an absolute date
                     */
                    retryInterval = Integer.parseInt(retryAfterField);
                    if (retryInterval > 0) {
                        if (retryInterval > 60) {
                            // only wait 1 min
                            retryInterval = 60;
                        }

                        Thread.sleep(retryInterval * 1000);
                    }
                } catch (InterruptedException ie) {
                    // ignore thread interrupt
                    break;
                } catch (NumberFormatException ne) {
                    // ignore bad format
                    break;
                }

                httpConnection.close();

                if (state.stopInstallation) {
                    postInstallMsgBackToProvider(USER_CANCELLED_MSG);
                    throw new IOException("stopped");
                }
            } // end for

            if (responseCode == HttpConnection.HTTP_NOT_FOUND) {
                throw new InvalidJadException(resourceNotFoundCode);
            }

            if (responseCode == HttpConnection.HTTP_NOT_ACCEPTABLE) {
                throw new InvalidJadException(invalidMediaTypeCode, "");
            }

            if (responseCode == HttpConnection.HTTP_UNAUTHORIZED) {
                // automatically throws the correct exception
                checkIfBasicAuthSupported(
                     httpConnection.getHeaderField("WWW-Authenticate"));

                state.exception = 
                    new InvalidJadException(InvalidJadException.UNAUTHORIZED);
                return 0;
            }

            if (responseCode == HttpConnection.HTTP_PROXY_AUTH) {
                // automatically throws the correct exception
                checkIfBasicAuthSupported(
                     httpConnection.getHeaderField("WWW-Authenticate"));

                state.exception =
                    new InvalidJadException(InvalidJadException.PROXY_AUTH);
                return 0;
            }

            if (responseCode != HttpConnection.HTTP_OK) {
                throw new IOException("Failed to download " + url +
                    " HTTP response code: " + responseCode);
            }

            mediaType = getMediaType(httpConnection.getType());
            if (mediaType != null) {
                boolean goodType = false;

                for (int i = 0; i < acceptableTypes.length; i++) {
                    if (mediaType.equals(acceptableTypes[i])) {
                        goodType = true;
                        break;
                    }
                }

                if (!goodType) {
                    throw new InvalidJadException(invalidMediaTypeCode,
                                                  mediaType);
                }
            } else if (!allowNoMediaType) {
                throw new InvalidJadException(invalidMediaTypeCode, "");
            }

            if (encoding != null) {
                encoding[0] = getCharset(httpConnection.getType());
            }

            httpInputStream = httpConnection.openInputStream();
            return transferData(httpInputStream, output);
        } finally {
            // Close the streams or connections this method opened.
            try {
                httpInputStream.close();
            } catch (Exception e) {
                // ignore
            }
                    
            try {
                conn.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * If the JAD belongs to an installed suite, check the URL against the
     * installed one. Set the state.exception if the user needs to be warned.
     *
     * @param url JAD or JAR URL of the suite being installed
     */
    private void checkForDifferentDomains(String url) {
        // perform a domain check not a straight compare
        if (state.ca == null && state.previousUrl != null) {
            HttpUrl old = new HttpUrl(state.previousUrl);
            HttpUrl current = new HttpUrl(url);

            if ((current.domain != null && old.domain == null) ||
                (current.domain == null && old.domain != null) ||
                (current.domain != null && old.domain != null &&
                 !current.domain.regionMatches(true, 0, old.domain, 0,
                                           old.domain.length()))) {
                /*
                 * The jad is at new location, could be bad,
                 * let the user decide
                 */
                state.exception = new InvalidJadException(
                                      InvalidJadException.JAD_MOVED,
                                      (state.previousUrl == null ? "none" :
                                      state.previousUrl));
                return;
            }
        }
    }

    /**
     * See if there is an installed version of the suite being installed and
     * if so, make an necessary checks. Will set state fields, including
     * the exception field for warning the user.
     *
     * @exception InvalidJadException if the new version is formated
     * incorrectly
     */
    private void checkPreviousVersion() throws InvalidJadException {
        MIDletSuite midletSuite;
        String installedVersion;
        int cmpResult;
        
        // Check if app already exists
        midletSuite = getMIDletSuite(state.storageName);

        try {
            checkVersionFormat(state.version);

            if (midletSuite == null) {
                // there is no previous version
                return;
            }

            state.isPreviousVersion = true;
            state.previousSuite = midletSuite;

            // get this now so we do not have to save the whole suite object
            state.previousJadUrl = midletSuite.getJadUrl();
            state.previousJarUrl = midletSuite.getJarUrl();
            state.previousUrl = midletSuite.getDownloadUrl();
            state.previousCA = midletSuite.getCA();
            if (state.previousCA == null) {
                state.previousCA = Resource.getString("the manufacturer");
            }

            if (state.force) {
                // do not ask questions, force an overwrite
                return;
            }

            // If it does, check version information
            installedVersion = midletSuite.getProperty(VERSION_PROP);
            cmpResult = vercmp(state.version, installedVersion);
            if (cmpResult < 0) {
                // older version, warn user
                state.exception = new InvalidJadException(
                                  InvalidJadException.OLD_VERSION,
                                  installedVersion);
                return;
            }

            if (cmpResult == 0) {
                // already installed, warn user
                state.exception = new InvalidJadException(
                                  InvalidJadException.ALREADY_INSTALLED,
                                  installedVersion);
                return;
            }

            // new version, warn user
            state.exception = new InvalidJadException(
                                  InvalidJadException.NEW_VERSION,
                                  installedVersion);
            return;
        } catch (NumberFormatException nfe) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.INVALID_VERSION);
        }
    }

    /**
     * If this is an update, make sure the RMS data is handle correctly
     * according to the OTA spec.
     * <p>
     * From the OTA spec:
     * <blockquote>
     * The RMS record stores of a MIDlet suite being updated MUST be
     * managed as follows:</p>
     * <ul>
     * <li>
     *   If the cryptographic signer of the new MIDlet suite and the
     *   original MIDlet suite are identical, then the RMS record
     *   stores MUST be retained and made available to the new MIDlet
     *   suite.</li>
     * <li>
     *   If the scheme, host, and path of the URL that the new
     *   Application Descriptor is downloaded from is identical to the
     *   scheme, host, and path of the URL the original Application
     *   Descriptor was downloaded from, then the RMS MUST be retained
     *   and made available to the new MIDlet suite.</li>
     * <li>
     *   If the scheme, host, and path of the URL that the new MIDlet
     *   suite is downloaded from is identical to the scheme, host, and
     *   path of the URL the original MIDlet suite was downloaded from,
     *   then the RMS MUST be retained and made available to the new
     *   MIDlet suite.</li>
     * <li>
     *   If the above statements are false, then the device MUST ask
     *   the user whether the data from the original MIDlet suite
     *   should be retained and made available to the new MIDlet
     *   suite.</li>
     * </ul>
     * </blockquote>
     *
     * @exception IOException if the install is stopped
     */
    private void processPreviousRMS() throws IOException {
        HttpUrl newUrl;
        HttpUrl originalUrl;
        String suiteStorageRoot = state.storageRoot + state.storageName;

        if (!RecordStoreFile.suiteHasRmsData(suiteStorageRoot)) {
            return;
        }

        if (state.previousCA != null && state.ca != null &&
                state.ca.equals(state.previousCA)) {
            // signers the same
            return;
        }

        try {
            newUrl = new HttpUrl(state.jadUrl);
            originalUrl = new HttpUrl(state.previousJadUrl);

            if (newUrl.scheme.equals(originalUrl.scheme) &&
                    newUrl.host.equals(originalUrl.host) &&
                    newUrl.path.equals(originalUrl.path)) {
                return;
            }
        } catch (NullPointerException npe) {
            // no match, fall through
        }

        try {
            newUrl = new HttpUrl(state.jarUrl);
            originalUrl = new HttpUrl(state.previousJarUrl);

            if (newUrl.scheme.equals(originalUrl.scheme) &&
                    newUrl.host.equals(originalUrl.host) &&
                    newUrl.path.equals(originalUrl.path)) {
                return;
            }
        } catch (NullPointerException npe) {
            // no match, fall through
        }

        // ask the user, if no listener assume no for user's answer

        if (state.listener != null) {
            if (state.listener.keepRMS(state)) {
                // user wants to keep the data
                return;
            }
        }

        // this is a good place to check for a stop installing call
        if (state.stopInstallation) {
            postInstallMsgBackToProvider(USER_CANCELLED_MSG);
            throw new IOException("stopped");
        }

        RecordStoreFile.removeRecordStoresForSuite(classSecurityToken,
                                                   suiteStorageRoot);
    }

    /**
     * Checks to make sure the HTTP server will support Basic authtication.
     *
     * @param wwwAuthField WWW-Authenticate field from the response header
     *
     * @exception InvalidJadException if server does not support Basic
     *                                authentication
     */
    private void checkIfBasicAuthSupported(String wwwAuthField) 
            throws InvalidJadException {
        wwwAuthField = wwwAuthField.trim();

        if (!wwwAuthField.regionMatches(true, 0, BASIC_TAG, 0,
                                        BASIC_TAG.length())) {
            throw new InvalidJadException(InvalidJadException.CANNOT_AUTH);
        }
    }

    /**
     * Parses out the media-type from the content-type field and convert
     * to lower case.
     * The media-type everything for the ';' that marks the parameters.
     *
     * @param contentType value of the content-type field
     *
     * @return media-type in lower case
     */
    private static String getMediaType(String contentType) {
        int semiColon;

        if (contentType == null) {
            return null;
        }

        semiColon = contentType.indexOf(';');
        if (semiColon < 0) {
            return contentType.toLowerCase();
        }

        return contentType.substring(0, semiColon).toLowerCase();
    }

    /**
     * Parses out the charset from the content-type field.
     * The charset parameter is after the ';' in the content-type field.
     *
     * @param contentType value of the content-type field
     *
     * @return charset
     */
    private static String getCharset(String contentType) {
        int start;
        int end;

        if (contentType == null) {
            return null;
        }

        start = contentType.indexOf("charset");
        if (start < 0) {
            return null;
        }

        start = contentType.indexOf('=', start);
        if (start < 0) {
            return null;
        }

        // start past the '='
        start++;

        end = contentType.indexOf(';', start);
        if (end < 0) {
            end = contentType.length();
        }
            
        return contentType.substring(start, end).trim();
    }

    /**
     * Formats the username and password for HTTP basic authentication
     * according RFC 2617.
     *
     * @param username for HTTP authentication
     * @param password for HTTP authentication
     *
     * @return properly formated basic authentication credential
     */
    private static String formatAuthCredentials(String username,
                                                String password) {
        byte[] data = new byte[username.length() + password.length() + 1];
        int j = 0;

        for (int i = 0; i < username.length(); i++, j++) {
            data[j] = (byte)username.charAt(i);
        }

        data[j] = (byte)':';
        j++;

        for (int i = 0; i < password.length(); i++, j++) {
            data[j] = (byte)password.charAt(i);
        }

        return "Basic " + Base64.encode(data, 0, data.length);
    }

    /**
     * Posts a status message back to the provider's URL in JAD.
     *
     * @param message status message to post
     */
    private void postInstallMsgBackToProvider(String message) {
        String url;

        // Send out delete notifications that have been queued, first
        postQueuedDeleteMsgsBackToProvider();

        // Now, send out install notifications
        url = state.getAppProperty(NOTIFY_PROP);
        postMsgBackToProvider(message, url);
    }

    /**
     * Posts all queued delete notification messages
     */
    private void postQueuedDeleteMsgsBackToProvider() {
        Vector      deleteNotifyList;
        Enumeration e;

        deleteNotifyList = getDeleteNotifyURLList();

        for (e = deleteNotifyList.elements(); e.hasMoreElements(); ) {
            String url = (String)e.nextElement();
            postMsgBackToProvider(DELETE_NOTIFICATION_MSG, url);
            removeFromDeleteNotifyURLList(url);
        }
    }

    /**
     * Posts a status message back to the provider's URL in JAD.
     *
     * @param message status message to post
     * @param url target http url for the status message
     */
    private void postMsgBackToProvider(String message, String url) {
        HttpConnection transaction;
        OutputStream out;

        if (url == null) {
            return;
        }

        try {
            transaction = (HttpConnection)Connector.open(url, Connector.WRITE);
            try {
                transaction.setRequestMethod(HttpConnection.POST);

                if ((state != null) && 
                    (state.proxyUsername != null) &&
                    (state.proxyPassword != null)) {
                    transaction.setRequestProperty("Proxy-Authorization",
                        formatAuthCredentials(state.proxyUsername,
                                              state.proxyPassword));
                }

                out = transaction.openOutputStream();

                try {
                    out.write(message.getBytes());
                } finally {
                    out.close();
                }

                // we will wait for the server to run the script
                transaction.getResponseCode();
            } finally {
                transaction.close();
            }
        } catch (IOException ioe) {
            // ignore IOExceptions
        } catch (IllegalArgumentException iae) {
            // ignore IllegalArgumentExceptions
        } catch (ClassCastException cce) {
            // ignore ClassCastExceptions
        }
    }

    /**
     * Queues provider's URL to send a delete notification message
     * during the next OTA session.
     *
     * @param url target url for the status message
     */
    private void addToDeleteNotifyURLList(String url) {
        Vector deleteNotifyList;

        if (url == null) {
            return;
        }

        // NOTE: The spec only guarantees that the http:// protocol be
        //        supported. We do not do any checking of the protocol.

        deleteNotifyList = getDeleteNotifyURLList();
        deleteNotifyList.addElement(url);
        saveDeleteNotifyURLList(deleteNotifyList);
    }

    /**
     * Retrieves the queued delete notification list. Each element 
     * in the Vector is the URL to send the delete notification to.
     *
     * @return the delete notification list stored in a Vector
     */
    private Vector getDeleteNotifyURLList() {
        RandomAccessStream storage;
        DataInputStream in;
        String line;

        if (deleteNotifyURLList != null) {
            return deleteNotifyURLList;
        }

        deleteNotifyURLList = new Vector();
        try {
            storage = new RandomAccessStream(classSecurityToken);

            storage.connect(File.getStorageRoot() + DELETE_NOTIFY_FILENAME, 
                            Connector.READ);
            try {
                in = storage.openDataInputStream();
                for (; ; ) {
                    line = in.readUTF();
                    if (line == null) {
                        break;
                    }

                    deleteNotifyURLList.addElement(line);
                }
            } finally {
                storage.disconnect();
            }
        } catch (IOException e) {
            /* will happen the first time, because it will not exist */
        }

        return deleteNotifyURLList;
    }

    /**
     * Removes the URL from the delete notification list.
     *
     * @param url a URL
     */
    private void removeFromDeleteNotifyURLList(String url) {
        Vector deleteNotifyList;

        deleteNotifyList = getDeleteNotifyURLList();
        deleteNotifyList.removeElement(url);
        saveDeleteNotifyURLList(deleteNotifyList);
    }

    /**
     * Writes the given delete notification list to disk.
     *
     * @param list Vector in the same format as one returned by
     *              getDeleteNotifyURLList()
     */
    private void saveDeleteNotifyURLList(Vector list) {
        File file;
        RandomAccessStream storage;
        String filename;
        DataOutputStream out;
        String line;

        file = new File(classSecurityToken);
        storage = new RandomAccessStream(classSecurityToken);
        filename = File.getStorageRoot() + DELETE_NOTIFY_FILENAME;
        try {
            file.delete(filename);
        } catch (Exception e) {
            // Ignore the exception that happens when the file does not exist
        }

        try {
            storage.connect(filename, RandomAccessStream.READ_WRITE_TRUNCATE);
            try {
                out = storage.openDataOutputStream();
                for (int i = 0; i < list.size(); i++) {
                    line = (String)list.elementAt(i);
                    out.writeUTF(line);
                }
            } finally {
                storage.disconnect();
            }
        } catch (IOException e) {
            /* Ignored. Should not happen. */
        }
    }

    /**
     * Function that actually does the work of transfering file data.
     * <p>
     * Updates the listener ever 1 K bytes.
     * <p>
     * If the amount of data to be read is larger than <code>maxDLSize</code>
     * we will break the input into chunks no larger than
     * <code>maxDLSize</code>. This prevents the VM from running out of
     * memory when processing large files.
     *
     * @param in the input stream to read from
     * @param out the output stream to write to
     *
     * @return number of bytes written to the output stream
     *
     * @exception IOException if any exceptions occur during transfer
     * of data
     */
    private int transferData(InputStream in, OutputStream out)
            throws IOException {
        byte[] buffer = new byte[MAX_DL_SIZE];
        int bytesRead;
        int totalBytesWritten = 0;

        if (state.listener != null) {
            state.listener.updateStatus(state.beginTransferDataStatus, state);
        }

        try {
            for (int nextUpdate = totalBytesWritten + 1024; ; ) {
                bytesRead = in.read(buffer);

                if (state.listener != null && (bytesRead == -1 ||
                        totalBytesWritten + bytesRead >= nextUpdate)) {

                    synchronized (state) {
                        if (state.stopInstallation) {
                            throw new IOException("stopped");
                        }

                        state.listener.updateStatus(state.transferStatus,
                                                    state);
                    }

                    nextUpdate = totalBytesWritten + 1024;
                }

                if (bytesRead == -1) {
                    return totalBytesWritten;
                }

                out.write(buffer, 0, bytesRead);
                totalBytesWritten += bytesRead;
            }
        } catch (IOException ioe) {
            if (state.stopInstallation) {
                postInstallMsgBackToProvider(USER_CANCELLED_MSG);
                throw new IOException("stopped");
            } else {
                throw ioe;
            }
        }
    }

    /**
     * Stops the installation. If installer is not installing then this
     * method has no effect. This will cause the install method to
     * throw an IOException if the install is not writting the suite
     * to storage which is the point of no return.
     *
     * @return true if the install will stop, false if it is too late
     */
    public boolean stopInstalling() {
        if (state == null) {
            return false;
        }

        synchronized (state) {
            if (state.writingSuite) {
                return false;
            }
        
            state.stopInstallation = true;

            try {
                httpInputStream.close();
            } catch (Exception e) {
                // ignore
            }

            try {
                httpConnection.close();
            } catch (Exception e) {
                // ignore
            }

            return true;
        }
    }

    /**
     * Tells if the installation was stopped by another thread.
     * @return true if the installation was stopped by another thread
     */
    public boolean wasStopped() {
        if (state == null) {
            return false;
        }

        return state.stopInstallation;
    }

    /**
     * Sets security domain for unsigned suites. The default is untrusted.
     * Can only be called by JAM for testing.
     *
     * @param domain name of a security domain
     */
    public void setUnsignedSecurityDomain(String domain) {
        MIDletSuite midletSuite = Scheduler.getScheduler().getMIDletSuite();

        // if a MIDlet suite is not scheduled, assume the JAM is calling.
        if (midletSuite != null) {
            midletSuite.checkIfPermissionAllowed(Permissions.MIDP);
        }

        unsignedSecurityDomain = domain;
    }

    /**
     * Checks to see that if any properties that are both in the JAD and
     * JAR manifest are not equal and throw a exception and notify the
     * server when a mismatch is found. Only used for trusted suites.
     * @throws InvalidJadException  if the properties do not match 
     */
    protected void checkForJadManifestMismatches()
            throws InvalidJadException {

        for (int i = 0; i < state.jarProps.size(); i++) {
            String key = state.jarProps.getKeyAt(i);
            String value = state.jarProps.getValueAt(i);
            String dup = state.jadProps.getProperty(key);

            if (dup == null) {
                continue;
            }

            if (!dup.equals(value)) {
                postInstallMsgBackToProvider(ATTRIBUTE_MISMATCH_MSG);
                throw new InvalidJadException(
                    InvalidJadException.ATTRIBUTE_MISMATCH, key);
            }
        }
    }

    /**
     * Builds the initial API permission for suite currently being installed.
     *
     * @param domain security domain name for the CA of the suite
     *
     * @return 2 arrays, maximum and current level of permissions
     *
     * @exception InvalidJadException if a permission attribute is not
     *     formatted properly or a required permission is denied
     */
    protected byte[][] getInitialPermissions(String domain)
            throws InvalidJadException {
        byte[][] domainPermissions = Permissions.forDomain(classSecurityToken,
                                                           domain);
        byte[] permissions = Permissions.getEmptySet();

        // only the current level of each permission has to be adjusted
        getRequestedPermissions(PERMISSIONS_PROP,
                                domainPermissions[Permissions.CUR_LEVELS],
                                permissions, true);

        getRequestedPermissions(PERMISSIONS_OPT_PROP,
                                domainPermissions[Permissions.CUR_LEVELS],
                                permissions, false);

        domainPermissions[Permissions.CUR_LEVELS] = permissions;
        return domainPermissions;
    }

    /**
     * Gets the permissions for a domain that are requested the manifest.
     *
     * @param propName name of the property in the manifest
     * @param domainPermissions array of the starting levels for permissions
     *        of a domain
     * @param permissions array to put the permissions from the domain in
     *        when found in the manifest property
     * @param required if set to true the manifest permissions are required
     *
     * @exception InvalidJadException if a permission attribute is not
     *     formatted properly or a required permission is denied
     */
    private void getRequestedPermissions(String propName,
            byte[] domainPermissions, byte[] permissions, boolean required)
            throws InvalidJadException {

        String reqPermissionLine;
        Vector manifestPermissions;
        String permission;
        boolean found;
        int i;

        reqPermissionLine = state.getAppProperty(propName);
        if (reqPermissionLine == null) {
            return;
        }

        manifestPermissions = Util.getCommaSeparatedValues(reqPermissionLine);
        if (manifestPermissions.size() == 0) {
            postInstallMsgBackToProvider(INVALID_JAD_MSG);
            throw new
                InvalidJadException(InvalidJadException.INVALID_VALUE);
        }

        for (int j = 0; j < manifestPermissions.size(); j++) {
            permission = (String)manifestPermissions.elementAt(j);

            if (permission.length() == 0) {
                postInstallMsgBackToProvider(INVALID_JAD_MSG);
                throw new
                    InvalidJadException(InvalidJadException.INVALID_VALUE);
            }

            found = false;
            for (i = 0; i < Permissions.NUMBER_OF_PERMISSIONS; i++) {
                if (Permissions.getName(i).equals(permission)) {
                    if (domainPermissions[i] != Permissions.NEVER) {
                        found = true;
                    }

                    break;
                }
            }

            if (!found) {
                if (required) {
                    postInstallMsgBackToProvider(AUTHORIZATION_FAILURE_MSG);
                    throw new InvalidJadException(
                        InvalidJadException.AUTHORIZATION_FAILURE, permission);
                }

                continue;
            }

            permissions[i] = domainPermissions[i];
        }
    }

    /**
     * Apply the previous user level permission of the currently installed
     * version of a suite to the next version of the suite in a secure way.
     *
     * @param current array permissions for the current version
     * @param domainPermissions array of the starting levels for permissions
     *        of the new domain
     * @param next array permissions for the next version
     */
    private void applyCurrentUserLevelPermissions(byte[] current,
            byte[] domainPermissions, byte[] next) {

        for (int i = 0; i < current.length && i < next.length; i++) {
            switch (current[i]) {
            case Permissions.ALLOW:
            case Permissions.DENY:
                // not a user level permission
                continue;
            }

            switch (domainPermissions[i]) {
            case Permissions.ALLOW:
            case Permissions.DENY:
                // not a user level permission
                continue;

            case Permissions.ONE_SHOT:
                if (current[i] == Permissions.SESSION ||
                        current[i] == Permissions.DENY_SESSION) {
                    // do not apply
                    continue;
                }
                // fall through
            case Permissions.SESSION:
                if (current[i] == Permissions.BLANKET ||
                        current[i] == Permissions.BLANKET_GRANTED) {
                    // do not apply
                    continue;
                }
                // fall through
            default:
                next[i] = current[i];
                continue;
            }
        }
    }

    /**
     * Gets the security domain name for this MIDlet Suite from storage.
     *
     * @param storageName given to the suite by the installer when it was
     *        downloaded
     * @param ca CA of an installed suite
     *
     * @return name of the security domain for the MIDlet Suite
     */
    protected String getSecurityDomainName(String storageName, String ca) {
        // This is a method that is overridden by a secure installer.
        return Permissions.UNTRUSTED_DOMAIN_NAME;
    }

    /**
     * Verifies a Jar. Post any error back to the server.
     *
     * @param jarStorage System store for applications
     * @param jarFilename name of the jar to read
     *
     * @exception IOException if any error prevents the reading
     *   of the JAR
     * @exception InvalidJadException if the JAR is not valid
     */
    protected void verifyJar(RandomAccessStream jarStorage,
                             String jarFilename)
        // This is a place holder for a secure installer.
        throws IOException, InvalidJadException {
    }

    /**
     * Compares two version strings. The return values are very similar to
     * that of strcmp() in 'C'. If the first version is less than the second
     * version, a negative number will be returned. If the first version is
     * greater than the second version, a positive number will be returned.
     * If the two versions are equal, zero is returned.
     * <p>
     * Versions must be in the form <em>xxx.yyy.zzz</em>, where:
     * <pre>
     *     <em>xxx</em> is the major version
     *     <em>yyy</em> is the minor version
     *     <em>zzz</em> is the micro version
     * </pre>
     * It is acceptable to omit the micro and possibly the minor versions.
     * If these are not included in the version string, the period immediately
     * preceding the number must also be removed. So, the versions
     * <em>xxx.yyy</em> or <em>xxx</em> are also valid.
     * <p>
     * Version numbers do not have to be three digits wide. However, you may
     * pad versions with leading zeros if desired.
     * <p>
     * If a version number is omitted, its value is assumed to be zero. All
     * tests will be based on this assumption.
     * <p>
     * For example:
     * <pre>
     *    1.04  >  1.
     *    1.04  <  1.4.1
     *    1.04  =  1.4.0
     * </pre>
     * <p>
     *
     * @param ver1 the first version to compare
     * @param ver2 the second version to compare
     *
     * @return  1 if <code>ver1</code> is greater than <code>ver2</code>
     *          0 if <code>ver1</code> is equal to <code>ver2</code>
     *         -1 if <code>ver1</code> is less than <code>ver2</code>
     *
     * @exception NumberFormatException if either <code>ver1</code> or 
     * <code>ver2</code> contain characters that are not numbers or periods
     */
    private int vercmp(String ver1, String ver2)
            throws NumberFormatException {
        String strVal1;
        String strVal2;
        int    intVal1;
        int    intVal2;
        int    idx1 = 0;
        int    idx2 = 0;
        int    newidx;

        if ((ver1 == null) && (ver2 == null)) {
            return 0;
        }

        if (ver1 == null) {
            return -1;
        }

        if (ver2 == null) {
            return 1;
        }

        for (int i = 0; i < 3; i++) {
            strVal1 = "0"; // Default value
            strVal2 = "0"; // Default value
            if (idx1 >= 0) {
                newidx = ver1.indexOf('.', idx1);
                if (newidx < 0) {
                    strVal1 = ver1.substring(idx1);
                } else {
                    strVal1 = ver1.substring(idx1, newidx);
                    newidx++; // Idx of '.'; need to go to next char
                }

                idx1 = newidx;
            }

            if (idx2 >= 0) {
                newidx = ver2.indexOf('.', idx2);
                if (newidx < 0) {
                    strVal2 = ver2.substring(idx2);
                } else {
                    strVal2 = ver2.substring(idx2, newidx);
                    newidx++;
                }

                idx2 = newidx;
            }

            intVal1 = Integer.parseInt(strVal1); // May throw NFE
            intVal2 = Integer.parseInt(strVal2); // May throw NFE

            if (intVal1 > intVal2) {
                return 1;
            }

            if (intVal1 < intVal2) {
                return -1;
            }
        }

        return 0;
    }

    /**
     * Checks the format of a version string.
     * <p>
     * Versions must be in the form <em>xxx.yyy.zzz</em>, where:
     * <pre>
     *     <em>xxx</em> is the major version
     *     <em>yyy</em> is the minor version
     *     <em>zzz</em> is the micro version
     * </pre>
     * It is acceptable to omit the micro and possibly the minor versions.
     * If these are not included in the version string, the period immediately
     * preceding the number must also be removed. So, the versions
     * <em>xxx.yyy</em> or <em>xxx</em> are also valid.
     * <p>
     * Version numbers do not have to be three digits wide. However, you may
     * pad versions with leading zeros if desired.
     *
     * @param ver the version to check
     *
     * @exception NumberFormatException if <code>ver</code> 
     *     contains any characters that are not numbers or periods
     */
    private void checkVersionFormat(String ver) throws NumberFormatException {
        int length;
        int start = 0;
        int end;

        length = ver.length();
        for (int i = 0; ; i++) {
            // check for more than 3 parts or a trailing '.'
            if (i == 3 || start == length) {
                throw new NumberFormatException();
            }

            end = ver.indexOf('.', start);
            if (end == -1) {
                end = length;
            }

            // throws NFE if the substring is not all digits
            Integer.parseInt(ver.substring(start, end));

            if (end == length) {
                // we are done
                return;
            }

            // next time around start after the index of '.'
            start = end + 1;
        }
    }

    /**
     * Match the name of the configuration or profile, and return 
     * true if the first name has a greater or equal version than the 
     * second. The names of the format "XXX-Y.Y" (e.g. CLDC-1.0, MIDP-2.0)
     * as used in the system properties (microedition.configuration &
     * microedition.profiles).
     *
     * This is used for checking both configuration and profiles.
     *
     * @param name1 name of configuration or profile 
     * @param name2 name of configuration or profile
     * @return  true is name1 matches name2 and is greater or equal in
     *          version number. false otherwise
     */
    private boolean matchVersion(String name1, String name2) {
        String base1 = name1.substring(0, name1.indexOf('-'));
        String base2 = name2.substring(0, name1.indexOf('-'));

        if (base1.equals(base2)) {
            String ver1 = name1.substring(name1.indexOf('-') + 1, 
                                          name1.length());
            String ver2 = name2.substring(name2.indexOf('-') + 1, 
                                          name2.length());
            return (vercmp(ver1, ver2) >= 0);
        } else {
            return false;
        }
    }
    
    /**
     * Checks to make sure the configration need by the application
     * is supported.
     * Send a message back to the server if the check fails and
     * throw an exception.
     *
     * @exception InvalidJadException if the check fails
     */
    private void checkConfiguration() throws InvalidJadException {
        String config;

        config = state.getAppProperty(CONFIGURATION_PROP);
        if (config == null || config.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAR_MSG);
            throw new InvalidJadException(
                InvalidJadException.MISSING_CONFIGURATION);
        }

        if (cldcConfig == null) {
            // need to call trim to remove trailing spaces
            cldcConfig = 
                System.getProperty("microedition.configuration").trim();
        }

        if (matchVersion(cldcConfig, config)) {
            // success, done
            return;
        }

        postInstallMsgBackToProvider(INCOMPATIBLE_MSG);
        throw new InvalidJadException(InvalidJadException.DEVICE_INCOMPATIBLE);
    }

    /**
     * Tries to match one of the supported profiles with a profile
     * listed in string of profiles separated by a space.
     * Send a message back to the server if a match is not found and
     * throw an exception.
     *
     * @exception InvalidJadException if there is no match
     */
    private void matchProfile() throws InvalidJadException {
        String profiles = state.getAppProperty(PROFILE_PROP);

        if (profiles == null || profiles.length() == 0) {
            postInstallMsgBackToProvider(INVALID_JAR_MSG);
            throw new
                InvalidJadException(InvalidJadException.MISSING_PROFILE);
        }

        // build the list of supported profiles if needed
        if (supportedProfiles == null) {
            int start;
            int nextSpace = -1;
            // need to call trim to remove trailing spaces
            String meProfiles = 
                System.getProperty("microedition.profiles").trim();
            supportedProfiles = new Vector();

            for (; ; ) {
                start = nextSpace + 1;
                nextSpace = meProfiles.indexOf(' ', start);

                // consecutive spaces, keep searching
                if (nextSpace == start) {
                    continue;
                }

                if ((nextSpace < 0)) {
                    supportedProfiles.addElement(
                        meProfiles.substring(start, meProfiles.length()));
                    break;
                }

                supportedProfiles.addElement(
                    meProfiles.substring(start, nextSpace));

            }
        }            
            
        /*
         * for each profiles listed in MicroEdition-Profile, we need to
         * find a matching profile in microedition.profiles.
         */
        int current = 0;
        int nextSeparatorIndex = 0;
        String requestedProfile;
        boolean supported = false;

        // convert tab to space so that the parsing later is simplified
        StringBuffer tmp = new StringBuffer(profiles);
        boolean modified = false;
        while ((nextSeparatorIndex = profiles.indexOf('\t', current)) != -1) {
            tmp.setCharAt(nextSeparatorIndex, ' ');
            current++;
            modified = true;
        }

        if (modified) {
            profiles = tmp.toString();
        }
            
        // reset the indices
        current = nextSeparatorIndex = 0;
        do {
            // get the next requested profiles
            nextSeparatorIndex = profiles.indexOf(' ', current);

            if (nextSeparatorIndex == current) {
                // consecutive spaces, keep searching
                current++;
                continue;
            }

            if (nextSeparatorIndex == -1) {
                // last (or the only one) value in the list
                requestedProfile = 
                   profiles.substring(current, profiles.length());
            } else {
                requestedProfile = 
                    profiles.substring(current, nextSeparatorIndex);
                current = nextSeparatorIndex + 1;
            }

            /* 
             * try to match each requested profiles against the supported
             * ones.
             */
            supported = false;
            for (int i = 0; i < supportedProfiles.size(); i++) {
                String supportedProfile = 
                    (String)supportedProfiles.elementAt(i);
                if (matchVersion(supportedProfile, requestedProfile)) {
                     supported = true;
                     break;
                }
            }   

            // short circuit the test if there is one mismatch
            if (!supported) {
                break;
            }
        } while (nextSeparatorIndex != -1);

        // matched all requested profiles against supported ones
        if (supported) {
            return;
        }

        postInstallMsgBackToProvider(INCOMPATIBLE_MSG);
        throw new InvalidJadException(InvalidJadException.DEVICE_INCOMPATIBLE);
    }

    /**
     * Registers the push connections for the application.
     * Send a message back to the server if a connection cannot be
     * registered and throw an exception.
     *
     * @exception InvalidJadException if a connection cannot be registered
     */
    private void registerPushConnections()
            throws InvalidJadException {
        byte[] curLevels = state.permissions[Permissions.CUR_LEVELS];

        if (state.isPreviousVersion) {
            PushRegistryImpl.unregisterConnections(classSecurityToken,
                                                   state.storageName);
        }

        for (int i = 1; ; i++) {
            String pushProp;

            pushProp = state.getAppProperty("MIDlet-Push-" + i);
            if (pushProp == null) {
                break;
            }

            /*       
             * Parse the comma separated values  -           
             *  " connection, midlet, role, filter"
             */
            int comma1 = pushProp.indexOf(',', 0);
            int comma2 = pushProp.indexOf(',', comma1 + 1);
            
            String conn = pushProp.substring(0, comma1).trim();
            String midlet = pushProp.substring(comma1+1, comma2).trim();
            String filter = pushProp.substring(comma2+1).trim();

            /* Register the new push connection string. */
            try {
                PushRegistryImpl.registerConnectionInternal(
                                                    classSecurityToken,
                                                    state,
                                                    conn, midlet, 
                                                    filter, false);
            } catch (Exception e) {
                /* If already registered, abort the installation. */
                PushRegistryImpl.unregisterConnections(classSecurityToken,
                                                       state.storageName);

                if (state.isPreviousVersion) {
                    // put back the old ones, removed above
                    redoPreviousPushConnections();
                }

                if (e instanceof SecurityException) {
                    postInstallMsgBackToProvider(AUTHORIZATION_FAILURE_MSG);

                    // since our state object put the permission in message
                    throw new InvalidJadException(
                        InvalidJadException.AUTHORIZATION_FAILURE,
                        e.getMessage());
                }

                postInstallMsgBackToProvider(PUSH_REG_FAILURE_MSG);

                if (e instanceof IllegalArgumentException) {
                    throw new InvalidJadException(
                        InvalidJadException.PUSH_FORMAT_FAILURE, pushProp);
                }

                if (e instanceof ConnectionNotFoundException) {
                    throw new InvalidJadException(
                        InvalidJadException.PUSH_PROTO_FAILURE, pushProp);
                }

                if (e instanceof IOException) {
                    throw new InvalidJadException(
                        InvalidJadException.PUSH_DUP_FAILURE, pushProp);
                }

                if (e instanceof ClassNotFoundException) {
                    throw new InvalidJadException(
                        InvalidJadException.PUSH_CLASS_FAILURE, pushProp);
                }

                // error in the RI code
                throw (RuntimeException)e;
            }
        }

        if (state.isPreviousVersion) {
            // use the old setting
            state.pushInterruptSetting =
                (byte)state.previousSuite.getPushInterruptSetting();

            // The old suite may have not had push connections
            if (state.pushInterruptSetting != Permissions.NEVER) {
                return;
            }
        }

        if (curLevels[Permissions.PUSH] == Permissions.NEVER) {
            state.pushInterruptSetting = Permissions.NEVER;
        } else if (curLevels[Permissions.PUSH] == Permissions.ALLOW) {
            state.pushInterruptSetting = Permissions.BLANKET;
        } else if (curLevels[Permissions.PUSH] == Permissions.ONE_SHOT) {
            // treat the ONE_SHOT as sesssion for interrupting
            state.pushInterruptSetting = Permissions.SESSION;
        } else {
            state.pushInterruptSetting = curLevels[Permissions.PUSH];
        }
    }

    /**
     * Registers the push connections for previous version after
     * and aborted update.
     */
    private void redoPreviousPushConnections() {
        for (int i = 1; ; i++) {
            String pushProp;

            pushProp = state.previousSuite.getProperty("MIDlet-Push-" + i);
            if (pushProp == null) {
                break;
            }

            /*       
             * Parse the comma separated values  -           
             *  " connection, midlet, role, filter"
             */
            int comma1 = pushProp.indexOf(',', 0);
            int comma2 = pushProp.indexOf(',', comma1 + 1);
            
            String conn = pushProp.substring(0, comma1).trim();
            String midlet = pushProp.substring(comma1+1, comma2).trim();
            String filter = pushProp.substring(comma2+1).trim();

            /* Register the new push connection string. */
            try {
                PushRegistryImpl.registerConnectionInternal(
                                                    classSecurityToken,
                                                    state,
                                                    conn, midlet, 
                                                    filter, true);
            } catch (IOException e) {
                // should not happen
            } catch (ClassNotFoundException e) {
                // should not happen
            }
        }
    }

    /**
     * Gets the MIDlet Suite from storage.
     *
     * @param storageName given to the suite by the installer
     *                   when it was downloaded
     * @return MIDlet Suite for use by the Scheduler
     */
    public MIDletSuite getMIDletSuite(String storageName) {
        return getMIDletSuite(storageName, null);
    }

    /**
     * Gets the MIDlet Suite from storage, and selects one midlet to be run.
     *
     * @param storageName the unique name of the suite given
     *        by the installer when it was downloaded
     * @param midletToRun the name of the initial MIDlet in this suite to run,
     *        can be null
     *
     * @return MIDlet Suite for use by the Scheduler
     */
    public MIDletSuite getMIDletSuite(String storageName,
                                      String midletToRun) {
        File file = new File(classSecurityToken);
        String storageRoot = File.getStorageRoot() + storageName;
        String ca;

        if (!suiteExists(storageName)) {
            return null;
        }

        ca = getCA(storageRoot);
        return new MIDletSuiteImpl(classSecurityToken,
                                   storageRoot,
                                   storageName,
                                   ca,
                                   midletToRun);
    }

    /**
     * Gets the MIDlet Suite from storage, and selects one midlet to be run.
     *
     * @param storageName the unique name of the suite given
     *        by the installer when it was downloaded
     * @param midletToRun the number of the initial MIDlet in this suite
     *        to run
     *
     * @return MIDlet Suite for use by the Scheduler
     */
    public MIDletSuite getMIDletSuite(String storageName,
                                      int midletToRun) {
        File file = new File(classSecurityToken);
        String storageRoot = File.getStorageRoot() + storageName;
        String ca;

        if (!suiteExists(storageName)) {
            return null;
        }

        ca = getCA(storageRoot);
        return new MIDletSuiteImpl(classSecurityToken,
                                   storageRoot,
                                   storageName,
                                   ca,
                                   midletToRun);
    }

    /**
     * Removes a software package given its storage name
     * <p>
     * If the component is in use it must continue to be available
     * to the other components that are using it.  The resources it
     * consumes must not be released until it is not in use.
     * 
     * @param storageName storage name for the installed package
     *
     * @exception SecurityException if the caller does not have permission
     *   to remove the installed package
     * @exception IllegalArgumentException if the suite cannot be found
     */
    public void remove(String storageName) {
        MIDletSuite suiteToDelete = getMIDletSuite(storageName, null);
        String deleteURL = null;

        synchronized (myInstaller) {
	    /*
	     * Block any push launch operations while the remove is
	     * in progress.
	     */
	    PushRegistryImpl.enablePushLaunch(classSecurityToken, false);
	    try {
		/*
		 * Before removing the suite, get info to send delete
		 * notification back to server.
		 */
		suiteToDelete = getMIDletSuite(storageName, null);
		if (suiteToDelete != null) {
		    deleteURL = suiteToDelete.getProperty(DELETE_PROP);
		    addToDeleteNotifyURLList(deleteURL);
		}
		
		if (!removeFromSuiteList(storageName)) {
		    throw new IllegalArgumentException("Could not find " +
						       storageName);
		}
		cleanUp(storageName);
	    } finally {
		PushRegistryImpl.enablePushLaunch(classSecurityToken, true);
	    }
        }
    }

    /**
     * Removes the resources a software package given its storage name
     * 
     * @param storageName storage name for the installed package
     *
     */
    private void cleanUp(String storageName) {
        File file = new File(classSecurityToken);
        String storageRoot = File.getStorageRoot() + storageName;
        Vector files;
        int numberOfFiles;

        /*
         * Before removing the suite, check for registered push
         * connections that might need to be unregistered.
         */
        PushRegistryImpl.unregisterConnections(classSecurityToken,
                                               storageName);

        files = file.filenamesThatStartWith(storageRoot);
        numberOfFiles = files.size();
        for (int i = 0; i < numberOfFiles; i++) {
            try {
                file.delete((String)files.elementAt(i));
            } catch (IOException ioe) {
                // ignore
            }
        }
    }

    /**
     * Queues the execution of the named Application suite to run.
     * The current application suite should terminate itself normally
     * to make resources available to the new application suite. Only
     * one package and set of MIDlets can be queued in this manner.
     * If multiple calls to execute are made, the package and MIDlets
     * specified during the <em>last</em> invokation will be executed
     * when the current application is terminated.
     *
     * @param storageName of an installed package
     * @param midlet name of MIDlet to invoke
     *
     * @return true if the current application MUST exit before
     * the new application suite can be executed
     *
     * @exception SecurityException if the caller does not have permission
     *   to execute the installed package
     */
    public boolean execute(String storageName, String midlet) 
        throws SecurityException {

        nextMidletSuiteToRun = storageName;
        nextMidletToRun = midlet; 
        return true;
    }

    /**
     * Gets the unique storage name of the next MIDlet suite to run.
     *
     * @return storage name of a MIDlet suite
     */
    public String getNextMIDletSuiteToRun() {
        return nextMidletSuiteToRun;
    }

    /**
     * Gets the name of the next MIDlet to run.
     *
     * @return storage name of a MIDlet
     */
    public String getNextMIDletToRun() {
        return nextMidletToRun; 
    }

    /**
     * List all installed software packages by storage name.
     *
     * @return an array of Strings of the storage names for the
     *     installed packages
     * @exception SecurityException if the caller does not have permission
     *     to see what software is installed
     */
    public String[] list() {
        Vector suiteList = getSuiteList();

        String[] array = new String[suiteList.size()];
        for (int i = 0; i < suiteList.size(); i++) {
            array[i] = (String)suiteList.elementAt(i);
        }

        return array;
    }

    /**
     * Tells if a suite exists.
     *
     * @param storageName storage name of a suite
     *
     * @return true if a suite of the given storage name
     *          already exists on the system
     */
    public boolean suiteExists(String storageName) {
        Vector suiteList;

        suiteList = getSuiteList();
        for (int i = 0; i < suiteList.size(); i++) {
            if (suiteList.elementAt(i).equals(storageName)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Retrieves the list of MIDlet suites and store them into a Vector
     * object. Each element in the Vector is the storage name
     * of an installed application.
     *
     * @return the list of installed suites stored in a Vector
     */
    private Vector getSuiteList() {
        RandomAccessStream storage;
        DataInputStream in;
        String line;

        if (midletSuiteList != null) {
            return midletSuiteList;
        }

        midletSuiteList = new Vector();
        try {
            storage = new RandomAccessStream(classSecurityToken);

            storage.connect(File.getStorageRoot() +
                           SUITE_LIST_FILENAME, Connector.READ);
            try {
                in = storage.openDataInputStream();
                for (; ; ) {
                    line = in.readUTF();
                    if (line == null) {
                        break;
                    }

                    midletSuiteList.addElement(line);
                }
            } finally {
                storage.disconnect();
            }
        } catch (IOException e) {
            /* will happen the first time, because it will not exist */
        }

        return midletSuiteList;
    }

    /**
     * Adds storage name into the list of MIDlet suites.
     *
     * @param storageName ID of a suite
     */
    private void addToSuiteList(String storageName) {
        Vector suiteList;

        if (suiteExists(storageName)) {
            return;
        }

        suiteList = getSuiteList();
        suiteList.addElement(storageName);
        saveSuiteList(suiteList);
    }

    /**
     * Removes the suite from the list of installed suites.
     *
     * @param storageName a suite
     *
     * @return true if the suite was in the list, else false
     */
    private boolean removeFromSuiteList(String storageName) {
        Vector suiteList;

        if (!suiteExists(storageName)) {
            return false;
        }

        suiteList = getSuiteList();
        suiteList.removeElement(storageName);
        saveSuiteList(suiteList);
        return true;
    }

    /**
     * Writes the given MIDlet suite list to disk.
     *
     * @param list Vector in the same format as one returned by getSuiteList()
     */
    private void saveSuiteList(Vector list) {
        File file;
        RandomAccessStream storage;
        String filename;
        DataOutputStream out;
        String line;

        file = new File(classSecurityToken);
        storage = new RandomAccessStream(classSecurityToken);
        filename = File.getStorageRoot() + SUITE_LIST_FILENAME;
        try {
            file.delete(filename);
        } catch (Exception e) {
            // ignore the exception that happen when the file does not exist
        }

        try {
            storage.connect(filename, RandomAccessStream.READ_WRITE_TRUNCATE);
            try {
                out = storage.openDataOutputStream();
                for (int i = 0; i < list.size(); i++) {
                    line = (String)list.elementAt(i);
                    out.writeUTF(line);
                }
            } finally {
                storage.disconnect();
            }
        } catch (IOException e) {
            /* ignored. Shouldn't happen */
        }
    }

    /**
     * Gets the CA of an installed package.
     *
     * @param storageRoot storage root name for the installed package
     *                    it should have any file separators need
     * @return string with the CN of the CA or
     *          <code>null</code> is returned if no value is available
     */
    String getCA(String storageRoot) {
        RandomAccessStream myStorage;
        DataInputStream dataStream;
        String ca;

        myStorage = new RandomAccessStream(classSecurityToken);

        try {
            myStorage.connect(storageRoot + CA_FILENAME,
                              Connector.READ);
            try {
                dataStream = myStorage.openDataInputStream();
                ca = dataStream.readUTF();
            } finally {
                myStorage.disconnect();
            }

            return ca;
        } catch (IOException ioe) {
            return null;
        }
    }

    /**
     * Holds the state of an installation, so it can restarted after it has
     * been stopped.
     */
    protected class InstallStateImpl implements InstallState, MIDletSuite {

        /** Receives warnings and status. */
        protected InstallListener listener;

        /** When the install started, in milliseconds. */
        protected long startTime;

        /** What to do next. */
        protected int nextStep;

        /** Signals the installation to stop. */
        protected boolean stopInstallation;

        /** Signals that installation is writting the suite. */
        protected boolean writingSuite;

        /** exception that stopped the installation. */
        protected InvalidJadException exception;

        /** URL of the JAD. */
        protected String jadUrl;

        /**
         * Option to force an overwrite of existing components without
         * any version comparison.
         */
        protected boolean force;

        /**
         * Option to force the RMS data of the suite to be overwritten to
         * be removed without comparison to the new suite.
         */
        protected boolean removeRMS;

        /** Raw JAD. */
        protected byte[] jad;

        /** character encoding of the JAD. */
        protected String jadEncoding;

        /** Parsed JAD. */
        protected JadProperties jadProps;

        /** Parsed manifest. */
        protected ManifestProperties jarProps;

        /** Cached File object. */
        protected File file;

        /** User name for authentication. */
        protected String username;

        /** Password for authentication. */
        protected String password;

        /** User name for proxyAuthentication. */
        protected String proxyUsername;

        /** Password for proxy authentication. */
        protected String proxyPassword;

        /** Name of the suite. */
        protected String suiteName;

        /** Vendor of the suite. */
        protected String vendor;

        /** Version of the suite. */
        protected String version;

        /** Description of the suite. */
        protected String description;

        /** What name the installed suite is stored by. */
        protected String storageName;

        /** CA authorizing the suite, for secure installing. */
        protected String ca;

        /** How big the JAD says the JAR is. */
        protected int expectedJarSize;

        /** URL of the JAR. */
        protected String jarUrl;

        /** Status to signal the beginning of the data transfer. */
        protected int beginTransferDataStatus;

        /** Status for the data transfer method to give to the listener. */
        protected int transferStatus;

        /** Push interrupt setting. */
        private byte pushInterruptSetting;

        /** Permissions. */
        private byte[][] permissions;

        /** Permissions. */
        private SecurityToken securityToken;

        /** Flag for trusted suites. */
        private boolean trusted;

        /** Holds the unziped JAR manifest to be saved. */
        protected byte[] manifest;

        /** The JAR file name before it is verified. */
        protected String tempFilename;

        /** Cache of storage object. */
        protected RandomAccessStream storage;

        /** The root of all suites. */
        protected String storageRoot;

        /** Signals that previous version exists. */
        protected boolean isPreviousVersion;

        /** Previous MIDlet suite info. */
        protected MIDletSuite previousSuite;

        /** Where the suite was download from the last time. Avoids reading. */
        protected String previousUrl;

        /** JAD URL of the previous suite. Avoids extra file reading. */
        protected String previousJadUrl;

        /** JAR URL of the previous suite. Avoids extra file reading */
        protected String previousJarUrl;

        /** CA of previous version. Avoids extra file reading. */
        private String previousCA;

        /**
         * Gets the last recoverable exception that stopped the install.
         * Non-recoverable exceptions are thrown and not saved in the state.
         *
         * @return last exception that stopped the install
         */
        public InvalidJadException getLastException() {
            return exception;
        }

        /**
         * Gets the unique name that the installed suite was stored with.
         *
         * @return storage name that can be used to load the suite
         */
        public String getStorageName() {
            return storageName;
        }

        /**
         * Sets the username to be used for HTTP authentication.
         *
         * @param theUsername 8 bit username, cannot contain a ":"
         */
        public void setUsername(String theUsername) {
            username = theUsername;
        }

        /**
         * Sets the password to be used for HTTP authentication.
         *
         * @param thePassword 8 bit password
         */
        public void setPassword(String thePassword) {
            password = thePassword;
        }

        /**
         * Sets the username to be used for HTTP proxy authentication.
         *
         * @param theUsername 8 bit username, cannot contain a ":"
         */
        public void setProxyUsername(String theUsername) {
            proxyUsername = theUsername;
        }

        /**
         * Sets the password to be used for HTTP proxy authentication.
         *
         * @param thePassword 8 bit password
         */
        public void setProxyPassword(String thePassword) {
            proxyPassword = thePassword;
        }

        /**
         * Gets a property of the application to be installed.
         * First from the JAD, then if not found, the JAR manifest.
         *
         * @param key key of the property
         *
         * @return value of the property or null if not found
         */
        public String getAppProperty(String key) {
            String value;

            if (state.jadProps != null) {
                value = state.jadProps.getProperty(key);
                if (value != null) {
                    return value;
                }
            }

            if (state.jarProps != null) {
                value = state.jarProps.getProperty(key);
                if (value != null) {
                    return value;
                }
            }

            return null;
        }

        /**
         * Gets the URL of the JAR.
         *
         * @return URL of the JAR
         */
        public String getJarUrl() {
            return jarUrl;
        }

        /**
         * Gets the expected size of the JAR.
         *
         * @return size of the JAR in K bytes rounded up
         */
        public int getJarSize() {
            return (expectedJarSize + 1023) / 1024;
        }

        /**
         * Checks for permission and throw an exception if not allowed.
         * May block to ask the user a question.
         *
         * @param permission ID of the permission to check for,
         *      the ID must be from
         *      {@link com.sun.midp.security.Permissions}
         * @param resource string to insert into the question, can be null
         *
         * @exception SecurityException if the permission is not
         *            allowed by this token
         * @exception InterruptedException if another thread interrupts the
         *   calling thread while this method is waiting to preempt the
         *   display.
         */
        public void checkForPermission(int permission, String resource)
                throws InterruptedException {
            HttpUrl url;
            String protocolName = null;

            if (listener == null) {
                /*
                 * no listener no OTA, we can't ask the user, so any
                 * user level permission is denied, so only check for allowed
                 */
                securityToken.checkIfPermissionAllowed(permission,
                    Permissions.getName(permission));
            }

            try {
                url = new HttpUrl(resource);
                if (url.scheme != null) {
                    protocolName = url.scheme;
                }
            } catch (Exception e) {
                // ignore
            }

            securityToken.checkForPermission(permission,
                Permissions.getTitle(permission),
                Permissions.getQuestion(permission),
                state.suiteName, resource, protocolName,
                Permissions.DENY,
                "Maybe. Ask me each time.", "No, Don't install %1.",
                Permissions.getName(permission));
        }

        /**
         * Indicates if the named MIDlet is registered in the suite
         * with MIDlet-<n> record in the manifest or
         * application descriptor.
         * @param midletName class name of the MIDlet to be checked
         *
         * @return true if the MIDlet is registered
         */
        public boolean isRegistered(String midletName) {
            String midlet;
            MIDletInfo midletInfo;

            for (int i = 1; ; i++) {
                midlet = getAppProperty("MIDlet-" + i);
                if (midlet == null) {
                    return false; // We went past the last MIDlet
                }

                /* Check if the names match. */
                midletInfo = new MIDletInfo(midlet);
                if (midletInfo.classname.equals(midletName)) {
                    return true;
                }
            }
        }

        /**
         * Gets a property of the suite. A property is an attribute from
         * either the application descriptor or JAR Manifest.
         *
         * @param key the name of the property
         * @return A string with the value of the property.
         * 		<code>null</code> is returned if no value 
         *          is available for the key.
         */
        public String getProperty(String key) {
            return getAppProperty(key);
        }

        /**
         * Gets the URL that the suite was downloaded from.
         *
         * @return URL of the JAD, or JAR for a JAR only suite, never null,
         * even in development environments
         */
        public String getDownloadUrl() {
            if (jadUrl != null) {
                return jadUrl;
            }

            return jarUrl;
        }

        /**
         * Gets the name of Certificate Authority (CA) that authorized this
         * suite.
         *
         * @return name of a CA or null if the suite was not signed
         */
        public String getCA() {
            return ca;
        }

        /**
         * Gets push setting for interrupting other MIDlets.
         * Reuses the Permissions.
         *
         * @return push setting for interrupting MIDlets the value
         *        will be permission level from {@link Permissions}
         */
        public int getPushInterruptSetting() {
            return pushInterruptSetting;
        }

        /**
         * Indicates if this suite is trusted.
         * (not to be confused with a domain named "trusted",
         *  this only shown to the user and not used for permissions)
         *
         * @return true if the suite is trusted false if not
         */
        public boolean isTrusted() {
            return trusted;
        };


        /**
         * Gets the JAD URL of the suite. This is only for the installer.
         *
         * @return URL of the JAD can be null
         */
        public String getJadUrl() {
            return jadUrl;
        }

        /**
         * Gets the path root of any file this suite.
         * Has any needed file separators appended.
         *
         * @return storage path root
         */
        public String getStorageRoot() {
            return storageRoot + storageName;
        }

        /**
         * Adds a property to the suite.
         *
         * @param key the name of the property
         * @param value the value of the property
         *
         * @exception SecurityException if the calling suite does not have
         *            internal API permission
         */
        public void addProperty(String key, String value) {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Provides the number of of MIDlets in this suite.
         *
         * @return number of MIDlet in the suite
         */
        public int getNumberOfMIDlets() {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Gets the classname of the initial MIDlet to run.
         *
         * @return classname of a MIDlet
         */  
        public String getInitialMIDletClassname() {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Checks to see the suite has the ALLOW level for specific permission.
         * This is used for by internal APIs that only provide access to
         * trusted system applications.
         * <p>
         * Only trust this method if the object has been obtained from the
         * Scheduler of the suite.
         *
         * @param permission permission ID from
         *      {@link com.sun.midp.security.Permissions}
         *
         * @exception SecurityException if the suite is not
         *            allowed to perform the specified action
         */
        public void checkIfPermissionAllowed(int permission) {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Gets the status of the specified permission.
         * If no API on the device defines the specific permission 
         * requested then it must be reported as denied.
         * If the status of the permission is not known because it might
         * require a user interaction then it should be reported as unknown.
         *
         * @param permission to check if denied, allowed, or unknown
         * @return 0 if the permission is denied; 1 if the permission is
         *    allowed; -1 if the status is unknown
         */
        public int checkPermission(String permission) {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Gets a named resource out of the JAR of this MIDlet suite.
         *
         * @param name name of the resource
         * @return raw bytes of the resource or null if not available
         */
        public byte[] getResource(String name) {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Gets the amount of storage on the device that this suite is using.
         * This includes the JAD, JAR, management data, and RMS.
         *
         * @return number of bytes of storage the suite is using
         */
        public int getStorageUsed() {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Gets list of permissions for this suite.
         *
         * @return array of permissions from {@link Permissions}
         */
        public byte[][] getPermissions() {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Saves any the settings (security or others) that the user may have
         * changed. Normally called by the scheduler after
         * the last running MIDlet in the suite is destoryed.
         * However it could be call during a suspend of the VM so
         * that persisent settings of the suite can be perserved.
         */
        public void saveSettings() {
            throw new RuntimeException("Not Implemented");
        }

        /**
         * Asks the user want to interrupt the current MIDlet with
         * a new MIDlet that has received network data.
         *
         * @param connection connection to place in the permission question or
         *        null for alarm
         *
         * @return true if the use wants interrupt the current MIDlet,
         * else false
         */
        public boolean permissionToInterrupt(String connection) {
            throw new RuntimeException("Not Implemented");
        }
    }
}