FileDocCategorySizeDatePackage
PaymentInfo.javaAPI DocphoneME MR2 API (J2ME)51308Wed May 02 18:00:44 BST 2007com.sun.j2me.payment

PaymentInfo.java

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

package com.sun.j2me.payment;

import java.util.Date;
import java.util.Vector;

import java.io.Writer;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

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

import com.sun.midp.crypto.*;
import com.sun.midp.pki.*;
import com.sun.midp.security.*;
import com.sun.midp.publickeystore.*;

import javax.microedition.pki.CertificateException;

/**
 * This class represents the payment information read from the application 
 * Manifest file or obtained from the associated update URL.
 *
 * @version 1.11
 */
public final class PaymentInfo {
    
    /** A value indicating that the auto request mode is disabled. */
    public static final int AUTO_REQUEST_OFF = 0;
    /** A value indicating that the auto request mode is set to accept. */
    public static final int AUTO_REQUEST_ACCEPT = 1;
    /** A value indicating that the auto request mode is set to reject. */
    public static final int AUTO_REQUEST_REJECT = 2; 

    /** The version number of the JAR-Manifest fields. */
    private static final String CURRENT_VERSION = 
            System.getProperty("microedition.payment.version");
    /** Pay version attribute name. */
    private static final String PAY_VERSION = 
            "Pay-Version";
    /** Pay-adapters attribute name. */
    private static final String PAY_ADAPTERS = 
            "Pay-Adapters";
    /** Pay-Debug-DemoMode attribute name. */
    private static final String PAY_DBG_DEMOMODE = 
            "Pay-Debug-DemoMode";
    /** Pay-Debug-FailInitialize attribute name. */
    private static final String PAY_DBG_FAILINITIALIZE = 
            "Pay-Debug-FailInitialize";
    /** Pay-Debug-FailIO attribute name. */
    private static final String PAY_DBG_FAILIO =
            "Pay-Debug-FailIO";
    /** Pay-Debug-MissedTransactions attribute name. */
    private static final String PAY_DBG_MISSEDTRANSACTIONS = 
            "Pay-Debug-MissedTransactions";
    /** Pay-Debug-RandomTests attribute name. */
    private static final String PAY_DBG_RANDOMTESTS =
            "Pay-Debug-RandomTests";
    /** Pay-Debug-AutoRequestMode attribute name. */
    private static final String PAY_DBG_AUTOREQUESTMODE =
            "Pay-Debug-AutoRequestMode";
    /** Pay-Debug-NoAdapter attribute name. */
    private static final String PAY_DBG_NOADAPTER =
            "Pay-Debug-NoAdapter";
    /** Pay-Update-Date attribute name. */    
    private static final String PAY_UPDATE_DATE =
            "Pay-Update-Date";
    /** Pay-Update-Stamp attribute name. */
    private static final String PAY_UPDATE_STAMP =
            "Pay-Update-Stamp";
    /** Pay-Update-URL attribute name. */
    private static final String PAY_UPDATE_URL =
            "Pay-Update-URL";
    /** Pay-Cache attribute name. */
    private static final String PAY_CACHE =
            "Pay-Cache";
    /** Pay-Providers attribute name. */
    private static final String PAY_PROVIDERS =
            "Pay-Providers";
    /** Prefix for constructing provider specific attribute name. */
    private static final String PAY_PREFIX =
            "Pay-";
    /** Prefix for constructing feature description attribute name. */
    private static final String PAY_FEATURE_PREFIX =
            "Pay-Feature-";
    /** Suffix for constructing provider info attribute name. */
    private static final String INFO_SUFFIX =
            "-Info";
    /** 
     * Suffix for constructing price and payment specific information
     * attribute name. 
     */
    private static final String TAG =
            "-Tag-";
    /** Pay-Certificate-(n)-(m) attribute name prefix. */
    private static final String PAY_CERTIFICATE_PREFIX =
            "Pay-Certificate-";
    /** Pay-Signature-XXX-XXX attribute name prefix. */
    private static final String PAY_SIGNATURE_PREFIX =
            "Pay-Signature-";
    /** Pay-Signature-RSA-SHA1 attribute name. */
    private static final String PAY_SIGNATURE_RSA_SHA1 =
            "Pay-Signature-RSA-SHA1";
    /** PKI prefixes are used for property strip. */
    private static final char[][] PKI_PREFIXES = {
        PAY_CERTIFICATE_PREFIX.toCharArray(),
        PAY_SIGNATURE_PREFIX.toCharArray()
    };
    /** List of supported adapters. */
    private static final String[] VALID_ADAPTER_NAMES = {
        "PPSMS"
    };
    /** Pointer to "yes" string. */
    private static final String YES_VALUE = "yes";
    /** Pointer to "no" string. */
    private static final String NO_VALUE = "no";
    /** Array of options could only exist in payment attributes. */
    private static final String[] YES_NO_OPTIONS = {
        YES_VALUE,
        NO_VALUE
    };
    /** Array of options can only exist in payment attributes. */    
    private static final String[] ACCEPT_REJECT_OPTIONS = {
        "accept",
        "reject"
    };
    /** Instance of Utils class */
    private static final Utils utilities = 
            PaymentModule.getInstance().getUtilities();

    /** List of MIDlet requested adapters. */
    private String[] adapters;
    /** Pay-Debug-DemoMode attribute value. */
    private boolean dbgDemoMode;
    /** Pay-Debug-FailInitialize attribute value. */
    private boolean dbgFailInitialize;
    /** Pay-Debug-FailIO attribute value. */
    private boolean dbgFailIO;
    /** Pay-Debug-MissedTransactions attribute value. */
    private int dbgMissedTransactions;
    /** Pay-Debug-RandomTests attribute value. */
    private boolean dbgRandomTests;
    /** Pay-Debug-AutoRequestMode attribute value. */
    private int dbgAutoRequestMode;
    /** Pay-Update-Date attribute value. */
    private Date updateDate;
    /** Pay-Update-Stamp attibute value. */
    private Date updateStamp;
    /** Pay-Update-URL attribute value. */
    private String updateURL;
    /** Pay-Cache attribute value. */
    private boolean cache;
    /** Payment info expiration date. */
    private Date expirationDate;
    /** Array of features price tags. */
    private int[] featureToTag;
    /** List of MIDlet supported payment providers. */
    private ProviderInfo[] providers;   

    /** Default constructor. */
    private PaymentInfo() {
    }
    
    /**
     * Creates an instance of the <code>PaymentInfo</code> class. It reads
     * information from the provided JAD and Manifest properties.
     *
     * @param jadProperties the JAD properties
     * @param jarProperties the Manifest properties
     * @return the instance of the <code>PaymentInfo</code> class
     * @throws PaymentException if some of the properties are incorrect, 
     *      incomplete, unsupported, etc.
     */
    public static PaymentInfo createFromProperties(
            Properties jadProperties, Properties jarProperties) 
                throws PaymentException {
        PaymentInfo paymentInfo = new PaymentInfo();
        
        paymentInfo.loadFromJadProperties(jadProperties);
        paymentInfo.loadFromJarProperties(jarProperties);
        
        return paymentInfo;        
    }
    
    /**
     * Validates JAD properties.
     *
     * @param jadProperties the JAD properties
     * @throws PaymentException if some of the properties are incorrect, 
     *      incomplete, unsupported, etc.
     */
    public static void validateJadProperties(Properties jadProperties) 
            throws PaymentException {
        PaymentInfo paymentInfo = new PaymentInfo();
        paymentInfo.loadFromJadProperties(jadProperties);
    }
    
    /**
     * Validates the given payment update and if correct it updates the internal
     * state of the object accordingly.
     *
     * @param data a byte array which contains the payment update
     * @param charset the character set of the payment update
     * @throws PaymentException if the payment update is incorrect
     */
    public void updatePaymentInfo(byte[] data, String charset) 
            throws PaymentException {
        Properties props;

        InputStream bis = new ByteArrayInputStream(data);
        try {
            try {
                props = utilities.loadProperties(bis, charset);
            } finally {
                bis.close();
            }
        } catch (UnsupportedEncodingException e) {
            throw new PaymentException(
                    PaymentException.UNSUPPORTED_UPDATE_CHARSET, 
                    charset, null);
        } catch (IOException e) {
            throw new PaymentException(
                    PaymentException.INVALID_PROPERTIES_FORMAT,
                    e.getMessage());
        }
        
        // find a trusted provider certificate in one of the certification 
        // chains of the payment update
        X509Certificate trustedCertificate = findTrustedCertificate(
                props);

        // get the public key for the trusted certificate
        PublicKey publicKey;
        try {
            publicKey = trustedCertificate.getPublicKey();
        } catch (CertificateException e) {
            throw new PaymentException(
                    PaymentException.INVALID_PROVIDER_CERT, 
                    trustedCertificate.getSubject(), null);
        }
        
        // get the encoded signature
        String encodedSignature = props.getProperty(PAY_SIGNATURE_RSA_SHA1);
        if (encodedSignature == null) {
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_SIGNATURE_RSA_SHA1, null);
        }
        
        byte[] signature;
        try {
            signature = Base64.decode(encodedSignature);
        } catch (IOException e) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    PAY_SIGNATURE_RSA_SHA1, "invalid or unsupported signature");
        }
        
        // get the data for verification
        String propString;
        byte[] testData;
        
        try {
             propString = new String(data, charset);
             testData = removePKIProperties(propString).getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new PaymentException(
                    PaymentException.UNSUPPORTED_UPDATE_CHARSET, 
                    charset, null);
        }
        
        // verify the signature
        try {
            Signature sigVerifier = Signature.getInstance("SHA1withRSA");

            sigVerifier.initVerify(publicKey);
            
            sigVerifier.update(testData, 0, testData.length);
            if (!sigVerifier.verify(signature)) {
                throw new PaymentException(
                    PaymentException.SIGNATURE_VERIFICATION_FAILED);
            }
        } catch (GeneralSecurityException e) {
            throw new PaymentException(
                    PaymentException.SIGNATURE_VERIFICATION_FAILED);
        }
        
        // validate and accept new values
        loadFromJppProperties(props);
        updateDate = new Date();
    }

    /**
     * Exports the payment information into the given character output stream.
     *
     * @param os the output stream
     * @throws IOException indicates an output error
     */
    public void export(Writer os) throws IOException {
        StringBuffer buffer = new StringBuffer();
        
        // Pay-Version: 1.0
        buffer.append(PAY_VERSION);
        buffer.append(": ");
        buffer.append(CURRENT_VERSION);
        buffer.append("\n");
        
        // Pay-Update-Date: <Date>
        if (updateDate != null) {
            buffer.append(PAY_UPDATE_DATE);
            buffer.append(": ");
            buffer.append(utilities.formatISODate(updateDate.getTime()));
            buffer.append("\n");
        }
        
        // Pay-Update-Stamp: <Date>
        buffer.append(PAY_UPDATE_STAMP);
        buffer.append(": ");
        buffer.append(utilities.formatISODate(updateStamp.getTime()));
        buffer.append("\n");
        
        // Pay-Update-URL: <UpdateURL>
        buffer.append(PAY_UPDATE_URL);
        buffer.append(": ");
        buffer.append(updateURL);
        buffer.append("\n");
        
        // Pay-Cache: [yes|no|<Expiration-Date>]
        buffer.append(PAY_CACHE);
        buffer.append(": ");
        if (expirationDate != null) {
            buffer.append(utilities.formatISODate(expirationDate.getTime()));
        } else {
            buffer.append(cache ? YES_VALUE : NO_VALUE);
        }
        buffer.append("\n");
        
        // Pay-Feature-<n>: <m>
        for (int i = 0; i < featureToTag.length; ++i) {
            buffer.append(PAY_FEATURE_PREFIX);
            buffer.append(i);
            buffer.append(": ");
            buffer.append(featureToTag[i]);
            buffer.append("\n");
        }
        
        os.write(buffer.toString());
        buffer.setLength(0);

        // Pay-Providers: <ProviderTitles>
        buffer.append(PAY_PROVIDERS);
        buffer.append(": ");
        buffer.append(providers[0].getName());
        for (int i = 1; i < providers.length; ++i) {
            buffer.append(", ");
            buffer.append(providers[i].getName());
        }
        buffer.append("\n");
        
        for (int i = 0; i < providers.length; ++i) {
            exportProvider(buffer, providers[i]);
        }
        
        os.write(buffer.toString());
    }

    /**
     * Test if the payment information can be used for payment as is or it
     * needs to be updated first from the update URL.
     *
     * @return <code>true</code> if the payment information needs to be updated
     */
    public boolean needsUpdate() {
        // 1. no cache => update
        if (!cache) {
            return true;
        }
        
        // 2. expired cache => update
        if (expirationDate != null) {
            long currentTime = System.currentTimeMillis();
            if (currentTime > expirationDate.getTime()) {
                return true;
            }
        }
        
        // 3. missing tags => update
        for (int i = 0; i < providers.length; ++i) {
            if (providers[i].getNumPriceTags() == 0) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Returns <code>true</code> if the payment information should be stored
     * for the next time.
     *
     * @return <code>true</code> if the payment information should be cached
     */
    public boolean cache() {
        return cache;
    }

    /**
     * Test for the system debug mode.
     *
     * @return <code>true</code> if the is running in the system debug mode
     */
    private native boolean isDebugMode();
    
    /**
     * Test for the debug demo mode.
     *
     * @return <code>true</code> if the debug demo mode should be activated
     */
    public boolean isDemoMode() {
        return dbgDemoMode && isDebugMode();
    }

    /**
     * Test for the debug fail initialize mode.
     *
     * @return <code>true</code> if the debug fail initialize mode should be
     *      activated
     */
    public boolean getDbgFailInitialize() {
        return dbgFailInitialize;
    }

    /**
     * Test for the debug fail IO mode.
     *
     * @return <code>true</code> if the debug fail IO mode should be activated
     */
    public boolean getDbgFailIO() {
        return dbgFailIO;
    }
    
    /**
     * Returns the number of fake missed transactions that should be generated
     * when the application starts.
     *
     * @return the number of missed transactions to generate or <code>-1</code>
     *      if this debug mode is disabled
     */
    public int getDbgMissedTransactions() {
        return dbgMissedTransactions;
    }
    
    /**
     * Test for the debug random tests mode.
     *
     * @return <code>true</code> if the debug random tests mode should be
     *      activated
     */
    public boolean getDbgRandomTests() {
        return dbgRandomTests;
    }
    
    /**
     * Returns the debug auto request mode setting.
     *
     * @return <code>AUTO_REQUEST_OFF</code> if the auto request mode is 
     *      disabled, <code>AUTO_REQUEST_ACCEPT</code> if the auto request mode
     *      is set to accept and <code>AUTO_REQUEST_REJECT</code> if it is set 
     *      to reject
     * @see #AUTO_REQUEST_OFF
     * @see #AUTO_REQUEST_ACCEPT
     * @see #AUTO_REQUEST_REJECT
     */
    public int getDbgAutoRequestMode() {
        return dbgAutoRequestMode;
    }
    
    /**
     * Returns the URL of the payment update.
     *
     * @return the update URL
     */
    public String getUpdateURL() {
        return updateURL;
    }
    
    /**
     * Returns the date of the last update or <code>null</code> if the payment
     * information has been never updated.
     *
     * @return the last update date or <code>null</code>
     */
    public Date getUpdateDate() {
        return updateDate;
    }

    /** 
     * Gets the time stamp of last update.
     *
     * @return the time stamp
     */
    public Date getUpdateStamp() {
        return updateStamp;
    }

    /** 
     * Gets the number of features the application can request the user to pay
     * for.
     *
     * @return the number of paid features
     */
    public int getNumFeatures() {
        return featureToTag.length;
    }
    
    /** 
     * Returns the price tag for the given feature id.
     *
     * @param index the feature id
     * @return the price tag
     */
    public int getPriceTagForFeature(int index) {
        return featureToTag[index];
    }
    
    /** 
     * Returns the number of providers which can be used to pay for the 
     * application features.
     *
     * @return the number of providers
     */
    public int getNumProviders() {
        return providers.length;
    }
    
    /**
     * Return the provider information for the given provider id.
     *
     * @param index the provider id
     * @return the provider information
     */
    public ProviderInfo getProvider(int index) {
        return providers[index];
    }
   
    /**
     * Returns <code>true</code> if the given vector contains duplicate values.
     *
     * @param vector the vector of strings
     * @return <code>true</code> if the vector contains duplicate values
     */
    private boolean hasDuplicates(Vector vector) {
        int lastIndex = vector.size() - 1;
        for (int i = 0; i < lastIndex; ++i) {
            if (vector.indexOf(vector.elementAt(i), i + 1) != -1) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Constructs an string array from the given vector of strings. The 
     * resulting array will contain the same strings as the vector and in the 
     * same order as appeared in the vector.
     *
     * @param vector the vector of strings
     * @return the array of strings
     */
    private String[] toStringArray(Vector vector) {
        String[] strings = new String[vector.size()];
        vector.copyInto(strings);
        
        return strings;
    }
    
    /**
     * Returns <code>true</code> if the given name is a valid adapter name.
     *
     * @param name the name to test
     * @return <code>true</code> if the name is a valid adapter name
     */
    private boolean validateAdapterName(String name) {
        if (name.startsWith("X-")) {
            return name.length() > 2;
        }
        
        for (int i = 0; i < VALID_ADAPTER_NAMES.length; ++i) {
            if (VALID_ADAPTER_NAMES[i].equals(name)) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Returns <code>true</code> if the given string value represents a valid
     * currency code.
     *
     * @param name the string to test
     * @return <code>true</code> if the string is a valid currency code
     */
    private boolean validateCurrencyCode(String name) {
        if (name.length() != 3) {
            return false;
        }
        
        for (int i = 0; i < 3; ++i) {
            if ((name.charAt(i) < 'A') ||
                    (name.charAt(i) > 'Z')) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * Parses an attribute which can have only one of the given predefined 
     * values. It returns the index of the attribute's value or the 
     * <code>defValue</code> if the attribute is not defined.
     *
     * @param props the properties to read the attribute from
     * @param attribute the name of the attribute
     * @param options the predefined values
     * @param defValue a value to return when the attribute is not defined
     * @return the index of a string from <code>options</code> which equals to
     *      the attribute's value or <code>defValue</code>
     * @throws PaymentException if the attribute's value doesn't match any of
     *      the predefined values
     */
    private int readOptionalSelection(Properties props,
            String attribute, String[] options, int defValue)
                throws PaymentException {
        String value = props.getProperty(attribute);
        if (value == null) {
            return defValue;
        }
        
        for (int i = 0; i < options.length; ++i) {
            if (options[i].equals(value)) {
                return i;
            }
        }
        
        StringBuffer buffer = new StringBuffer();
        
        buffer.append("expecting ");
        buffer.append(options[0]);
        int i;
        for (i = 1; i < (options.length - 1); ++i) {
            buffer.append(", ");
            buffer.append(options[i]);
        }
        buffer.append(" or ");
        buffer.append(options[i]);

        throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    attribute, buffer.toString());
    }

    /**
    *   Parse and check the version number of the JAR-Manifest
    *   or JAD fields.
    * 
    *   @param payVersion version string to check
    *   @throws PaymentException if the parameter contains wrong
    *                            value or its value is greater than
    *                            the version of the Payment API
    *                            implemented in the device.
    */
    private void checkPayVersion(String payVersion) throws PaymentException{
        double curVer;
        double appVer;
        payVersion = payVersion.trim();

        // The format must be <major>.<minor>.
        // First accepted version is 1.0
        if ('1' > payVersion.charAt(0)    || 
            -1 == payVersion.indexOf('.') ||
            2 > payVersion.length() - payVersion.indexOf('.')  ) {
            // unsupported payment version
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    PAY_VERSION, null);
        }

        try {
            curVer = Float.parseFloat(CURRENT_VERSION);
            appVer = Float.parseFloat(payVersion);
        } catch ( NumberFormatException nfe ) {
            // unsupported payment version
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    PAY_VERSION, null);
        }
    
        if (curVer < appVer) {
            // unsupported payment version
            throw new PaymentException(
                    PaymentException.UNSUPPORTED_PAYMENT_INFO, 
                    PAY_VERSION, null);
        }
    }
    /**
     * Updates the payment information from the given JAD file properties. 
     * If an exception is thrown during the update the original object state 
     * remains intact.
     *
     * @param props the JAD file properties
     * @throws PaymentException if the data read are incorrect or incomplete
     */
    private void loadFromJadProperties(Properties props) 
            throws PaymentException {
        String payVersion = props.getProperty(PAY_VERSION);
        String payAdapters = props.getProperty(PAY_ADAPTERS);
     
        if (payVersion != null) {
            checkPayVersion(payVersion);
        
            if (payAdapters == null) {
                // missing PAY_ADAPTERS attribute
                throw new PaymentException(
                        PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                        PAY_ADAPTERS, null);
            }
        } else {
            if (payAdapters != null) {
                // missing PAY_VERSION attribute
                throw new PaymentException(
                        PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                        PAY_VERSION, null);
            }
        }
        
        String[] adapters = null;
        // read & validate adapters
        if (payVersion != null) {
            Vector names = Util.getCommaSeparatedValues(payAdapters);

            if (names.size() == 0) {
                throw new PaymentException(
                        PaymentException.INVALID_ATTRIBUTE_VALUE, 
                        PAY_ADAPTERS, "the value is empty");
            }
            
            // avoid duplicate names
            if (hasDuplicates(names)) {
                throw new PaymentException(
                        PaymentException.INVALID_ATTRIBUTE_VALUE, 
                        PAY_ADAPTERS, "duplicate fields in the value");
            }
            
            adapters = toStringArray(names);
            
            // validate adapter names
            for (int i = 0; i < adapters.length; ++i) {
                if (!validateAdapterName(adapters[i])) {
                    throw new PaymentException(
                            PaymentException.INVALID_ATTRIBUTE_VALUE, 
                            PAY_ADAPTERS, adapters[i] + " is not a valid " +
                                "adapter name");
                }
            }
            
            // validate supported adapters
            PaymentModule paymentModule = PaymentModule.getInstance();
            int j;
            
            for (j = 0; j < adapters.length; ++j) {
                if (paymentModule.isSupportedAdapter(adapters[j])) {
                    break;
                }
            }

            if (j == adapters.length) {
                throw new PaymentException(
                        PaymentException.UNSUPPORTED_ADAPTERS,
                        PAY_ADAPTERS, null);
            }
        }

        // read and validate Pay-Debug-* attributes
        boolean dbgDemoMode = readOptionalSelection(props, 
                PAY_DBG_DEMOMODE, YES_NO_OPTIONS, 1) != 1;
        boolean dbgFailInitialize = readOptionalSelection(props, 
                PAY_DBG_FAILINITIALIZE, YES_NO_OPTIONS, 1) != 1;
        boolean dbgFailIO = readOptionalSelection(props, 
                PAY_DBG_FAILIO, YES_NO_OPTIONS, 1) != 1;
        int dbgMissedTransactions = -1;
        boolean dbgRandomTests = readOptionalSelection(props, 
                PAY_DBG_RANDOMTESTS, YES_NO_OPTIONS, 1) != 1;
        int dbgAutoRequestMode = readOptionalSelection(props,
                PAY_DBG_AUTOREQUESTMODE, ACCEPT_REJECT_OPTIONS, -1) + 1;

        // Peyment spec 1.1
        // It is not used yet, but need for TCK passing
        boolean dbgNoAdapter = readOptionalSelection(props,
                PAY_DBG_NOADAPTER, YES_NO_OPTIONS, 1) != 1;

        String dbgMissedTransactionsStr = props.getProperty(
                PAY_DBG_MISSEDTRANSACTIONS);
        if (dbgMissedTransactionsStr != null) {
            try {
                dbgMissedTransactions = Integer.parseInt(
                        dbgMissedTransactionsStr);
            } catch (NumberFormatException e) {
            }
            
            if (dbgMissedTransactions < 0) {
                throw new PaymentException(
                        PaymentException.INVALID_ATTRIBUTE_VALUE, 
                        PAY_DBG_MISSEDTRANSACTIONS, 
                        "expecting a positive number");
            }
        }
        
        // everything is correct, let's change the object state
        this.adapters = adapters;
        this.dbgDemoMode = dbgDemoMode;
        this.dbgFailInitialize = dbgFailInitialize;
        this.dbgFailIO = dbgFailIO;
        this.dbgMissedTransactions = dbgMissedTransactions;
        this.dbgRandomTests = dbgRandomTests;
        this.dbgAutoRequestMode = dbgAutoRequestMode;
    }
    
    /**
     * Parses and returns the provider information for the given provider name
     * from the properties.
     *
     * @param props the properties to get provider from
     * @param provider the provider name
     * @return the provider information
     * @throws PaymentException if the provider information is incorrect or
     *      incomplete
     */
    private ProviderInfo loadProvider(Properties props, String provider) 
            throws PaymentException {
        String tempValue;
        String tempKey = PAY_PREFIX + provider + INFO_SUFFIX;
        
        tempValue = props.getProperty(tempKey);
        
        if (tempValue == null) {
            // missing or incorrect provider
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    tempKey, null);
        }

        int offset = 0;
        int index;
        
        index = tempValue.indexOf(',');
        if (index == -1) {
            // missing currency code
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    tempKey, "the currency code is not present");
        }

        String adapter = tempValue.substring(offset, index).trim();
        // validate adapter name
        if (!validateAdapterName(adapter)) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    tempKey, adapter + " is not a valid adapter name");
        }
        
        offset = index + 1;
        index = tempValue.indexOf(',', offset);
        
        String currency;
        if (index == -1) {
            currency = tempValue.substring(offset).trim();
        } else {
            currency = tempValue.substring(offset, index).trim();
        }
        // validate currency
        if (!validateCurrencyCode(currency)) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    tempKey, "not a valid currency code");
        }

        // get configuration
        String configuration;
        if (index == -1) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    tempKey, "the payment specific info is not present");
        }

        offset = index + 1;
        configuration = tempValue.substring(offset).trim();
        
        tempKey = PAY_PREFIX + provider + TAG;
        tempValue = props.getProperty(tempKey + 0);
        double[] prices = null;
        String[] paySpecificPriceInfo = null;
        if (tempValue != null) {
            // contains tag attributes
            int numTags = 0;
            Vector tempVector = new Vector();
            do {
                tempVector.addElement(tempValue);
                tempValue = props.getProperty(tempKey + ++numTags);
            } while (tempValue != null);

            // we know the number of tags
            prices = new double[numTags];
            paySpecificPriceInfo = new String[numTags];

            for (int i = 0; i < numTags; ++i) {
                tempValue = (String)tempVector.elementAt(i);
                
                index = tempValue.indexOf(',');
                
                // parse and validate the price
                try {
                    String tempPrice;
                    if (index == -1) {
                        tempPrice = tempValue.trim();
                    } else {
                        tempPrice = tempValue.substring(0, index).trim();
                    }
                    prices[i] = Double.parseDouble(tempPrice);
                } catch (NumberFormatException e) {
                    throw new PaymentException(
                            PaymentException.INVALID_ATTRIBUTE_VALUE, 
                            tempKey + i, "invalid price");
                }
                
                // get pay specific price info if present
                if (index != -1) {
                    paySpecificPriceInfo[i] = 
                            tempValue.substring(index + 1).trim();
                }
            }
        }
        
        // everything is correct, create the object
        return new ProviderInfo(provider, adapter, configuration, currency, 
                prices, paySpecificPriceInfo);
    }
    
    /**
     * Loads the payment information from the given Manifest properties or
     * update file properties. The <code>strict</code> indicates if the 
     * additional tests should be executed on the data read from the properties.
     * After passing these additional tests the resulting payment information 
     * can be used for payment without any further update. Should an exception
     * be thrown during the loading the object state will remain intact.
     *
     * @param props the properties
     * @param strict if <code>true</code> the requirements on the data read
     *      from the properties are harder
     * @throws PaymentException if the data read are incorrect or incomplete
     */
    private void loadFromPropertiesAux(Properties props, boolean strict)
            throws PaymentException {
        String tempValue;
        
        long currentTime = System.currentTimeMillis();
        
        tempValue = props.getProperty(PAY_VERSION);
        
        if (tempValue == null) {
            // missing PAY_VERSION attribute
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_VERSION, null);
        }

        // throws Payment exception 
        // if app pay version is greater than stack version
        checkPayVersion(tempValue);
        
        tempValue = props.getProperty(PAY_UPDATE_STAMP);
        Date updateStamp;
        
        if (tempValue == null) {
            // missing PAY_UPDATE_STAMP attribute
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_UPDATE_STAMP, null);
        }
        
        // parse and validate the date
        try {
            long millis = utilities.parseISODate(tempValue);
            if (millis > currentTime) {
                throw new PaymentException(
                        PaymentException.INFORMATION_NOT_YET_VALID);
            }
            updateStamp = new Date(millis);
        } catch (IllegalArgumentException e) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    PAY_UPDATE_STAMP, e.getMessage());
        }
        
        String updateURL = props.getProperty(PAY_UPDATE_URL);
        
        if (updateURL == null) {
            // missing PAY_UPDATE_URL attribute
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_UPDATE_URL, null);
        }
        
        // validate the URL
        try {
            HttpUrl tempURL = new HttpUrl(updateURL);
            if (!"http".equals(tempURL.scheme) && 
                    !"https".equals(tempURL.scheme)) {
                throw new PaymentException(
                        PaymentException.UNSUPPORTED_URL_SCHEME,
                        tempURL.scheme, null);
            }

        } catch (IllegalArgumentException e) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE,
                    PAY_UPDATE_URL, e.getMessage());
        }
        
        tempValue = props.getProperty(PAY_CACHE);
        boolean cache = true;
        Date expirationDate = null;
        
        // validate and parse the PAY_CACHE attribute
        if (tempValue != null) {
            if (YES_VALUE.equals(tempValue)) {
                cache = true;
            } else if (NO_VALUE.equals(tempValue)) {
                cache = false;
            } else {
                try {
                    long millis = utilities.parseISODate(tempValue);
                    if (strict && (millis < currentTime)) {
                        throw new PaymentException(
                                PaymentException.INFORMATION_EXPIRED);
                    }
                    expirationDate = new Date(millis);
                } catch (IllegalArgumentException e) {
                    throw new PaymentException( 
                            PaymentException.INVALID_ATTRIBUTE_VALUE,
                            PAY_CACHE, "expecting yes, no or a valid date");
                }
            }
        }
        
        Vector tempVector = new Vector();

        tempValue = props.getProperty(PAY_FEATURE_PREFIX + 0);

        if (tempValue == null) {
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_FEATURE_PREFIX + 0, null);
        }

        // read Pay-Feature-<n>
        int index = 0;
        tempVector.setSize(0);
        do {
            tempVector.addElement(tempValue);
            tempValue = props.getProperty(PAY_FEATURE_PREFIX + ++index);
        } while (tempValue != null);

        int maxTag = 0;
        int[] featureToTag = new int[index];
        // parse and validate the numbers
        for (int i = 0; i < index; ++i) {
            int value = -1;
            try {
                value = Integer.parseInt((String)tempVector.elementAt(i));
            } catch (NumberFormatException e) {
            }

            if (value < 0) {
                throw new PaymentException(
                        PaymentException.INVALID_ATTRIBUTE_VALUE,
                        PAY_FEATURE_PREFIX + i, "expecting a positive number");
            }
            
            if (maxTag < value) {
                maxTag = value;
            }

            featureToTag[i] = value;
        }

        tempValue = props.getProperty(PAY_PROVIDERS);
        
        if (tempValue == null) {
            // missing PAY_PROVIDERS attribute
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE, 
                    PAY_PROVIDERS, null);
        }
        
        Vector names = Util.getCommaSeparatedValues(tempValue);

        if (names.size() == 0) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    PAY_PROVIDERS, "the value is empty");
        }

        // avoid duplicate names
        if (hasDuplicates(names)) {
            throw new PaymentException(
                    PaymentException.INVALID_ATTRIBUTE_VALUE, 
                    PAY_PROVIDERS, "duplicate fields in the value");
        }
        
        // IMPL_NOTE: check provider name?
        
        ProviderInfo[] providers = new ProviderInfo[names.size()];
        int numTags = maxTag + 1;
        boolean hasSupportedProvider = false;
        PaymentModule paymentModule = PaymentModule.getInstance();
        // read and validate provider infos
        for (int i = 0; i < providers.length; ++i) {
            // read provider
            ProviderInfo provider = loadProvider(
                    props, (String)names.elementAt(i));

            if ((strict || (provider.getNumPriceTags() != 0))
                    && (provider.getNumPriceTags() < numTags)) {
                throw new PaymentException(
                        PaymentException.INCOMPLETE_INFORMATION);
            }
            
            // try to create an adapter for the provider
            PaymentAdapter adapter;
            try {
                adapter = paymentModule.getAdapter(provider.getAdapter(),
                    provider.getConfiguration());
            } catch (PaymentException e) {
                e.setParam(PAY_PREFIX + provider.getName() + INFO_SUFFIX);
                throw e;
            }
            
            if (adapter != null) {
                // adapter has been created == we support at least one payment
                // provider
                hasSupportedProvider = true;
                
                int numTags2 = provider.getNumPriceTags();
                for (int j = 0; j < numTags2; ++j) {
                    try {
                        adapter.validatePriceInfo(provider.getPrice(j),
                                provider.getPaySpecificPriceInfo(j));
                    } catch (PaymentException e) {
                        e.setParam(PAY_PREFIX + provider.getName() + TAG + j);
                        throw e;
                    }
                }
            }
            
            providers[i] = provider;
        }

        if (!hasSupportedProvider) {
            throw new PaymentException(
                    PaymentException.UNSUPPORTED_PROVIDERS,
                    PAY_PROVIDERS, null);
        }
        
        // everything is correct, let's change the object state
        this.updateStamp = updateStamp;
        this.updateURL = updateURL;
        this.cache = cache;
        this.expirationDate = expirationDate;
        this.featureToTag = featureToTag;
        this.providers = providers;
    }
    
    /**
     * Updates the payment information from the given Manifest properties. If 
     * an exception is thrown during the update the original object state 
     * remains intact.
     *
     * @param props the Manifest properties
     * @throws PaymentException if the data read are incorrect or incomplete
     */
    private void loadFromJarProperties(Properties props) 
            throws PaymentException {
        loadFromPropertiesAux(props, false);
        
        // load the update date
        String tempValue = props.getProperty(PAY_UPDATE_DATE);
        updateDate = null;
        if (tempValue != null) {
            try {
                long millis = utilities.parseISODate(tempValue);
                updateDate = new Date(millis);
            } catch (IllegalArgumentException e) {
            }
        }
    }

    /**
     * Updates the payment information from the given update file properties. 
     * If an exception is thrown during the update the original object state 
     * remains intact.
     *
     * @param props the update file properties
     * @throws PaymentException if the data read are incorrect or incomplete
     */
    private void loadFromJppProperties(Properties props) 
            throws PaymentException {
        loadFromPropertiesAux(props, true);
    }
    
    /**
     * Exports the given provider information into the given 
     * <code>StringBuffer</code>.
     *
     * @param buffer the <code>StringBuffer</code>
     * @param provider the provider information
     */
    private void exportProvider(StringBuffer buffer, ProviderInfo provider) {
        
        // Pay-<ProviderTitle>
        String providerPrefix = PAY_PREFIX + provider.getName();
        
        // Pay-<ProviderTitle>-Info: <RegAdapter>, <ISO4217CurrencyCode>,
        //      <PaymentSpecificInformation>
        buffer.append(providerPrefix);
        buffer.append(INFO_SUFFIX);
        buffer.append(": ");
        buffer.append(provider.getAdapter());
        buffer.append(", ");
        buffer.append(provider.getCurrency());
        buffer.append(", ");
        buffer.append(provider.getConfiguration());
        buffer.append("\n");

        // Pay-<ProviderTitle>-Tag-<m>: <Price>[, 
        //      <PaymentSpecificPriceInformation>]
        int count = provider.getNumPriceTags();
        for (int i = 0; i < count; ++i) {
            buffer.append(providerPrefix);
            buffer.append(TAG);
            buffer.append(i);
            buffer.append(": ");
            buffer.append(provider.getPrice(i));
            String value = provider.getPaySpecificPriceInfo(i);
            if (value != null) {
                buffer.append(", ");
                buffer.append(value);
            }
            buffer.append("\n");
        }
    }

    /**
     * Finds a trusted provider certificate in one of the certification 
     * chains read from the given properties.
     *
     * @param props the properties
     * @return the trusted provider certificate
     * @throws PaymentException if some certification chain is incorrect or
     *      none of them can be trusted
     */
    private static X509Certificate findTrustedCertificate(Properties props)
            throws PaymentException {
        int certPath = 1;
        int certIndex = 1;
        Vector certificates = new Vector();
        String encodedCert = props.getProperty(PAY_CERTIFICATE_PREFIX + 
                certPath + "-" + certIndex);

        if (encodedCert == null) {
            throw new PaymentException(
                    PaymentException.MISSING_MANDATORY_ATTRIBUTE,
                    PAY_CERTIFICATE_PREFIX + certPath + "-" + certIndex,
                    null);
        }
        
        do {
            certificates.setSize(0);
            
            do {
                try {
                    byte[] binaryCert = Base64.decode(encodedCert);
                    certificates.addElement(X509Certificate.generateCertificate(
                            binaryCert, 0, binaryCert.length));
                } catch (IOException e) {
                    throw new PaymentException(
                        PaymentException.INVALID_ATTRIBUTE_VALUE, 
                        PAY_CERTIFICATE_PREFIX + certPath + "-" + certIndex, 
                        "invalid or unsupported certificate");
                }
                
                encodedCert = props.getProperty(PAY_CERTIFICATE_PREFIX +
                        certPath + "-" + ++certIndex);
            } while (encodedCert != null);
            
            try {
                String[] authPath = X509Certificate.verifyChain(certificates, 
                        X509Certificate.DIGITAL_SIG_KEY_USAGE,
                        X509Certificate.CODE_SIGN_EXT_KEY_USAGE,
                        WebPublicKeyStore.getTrustedKeyStore());
                String domain = Permissions.UNIDENTIFIED_DOMAIN_BINDING;
                Vector keys = WebPublicKeyStore.getTrustedKeyStore().
                     findKeys(authPath[0]);
                if (keys != null) {
                    domain = ((PublicKeyInfo)keys.elementAt(0)).getDomain();
                }

                if (!Permissions.UNIDENTIFIED_DOMAIN_BINDING.equals(domain)) {
                    // we verified the chain
                    return (X509Certificate)certificates.elementAt(0);
                }

                // try next chain
                
            } catch (CertificateException e) {
                switch (e.getReason()) {
                    case CertificateException.UNRECOGNIZED_ISSUER:
                        // try next chain
                        break;
                    case CertificateException.EXPIRED:
                    case CertificateException.NOT_YET_VALID:
                        throw new PaymentException(
                                PaymentException.EXPIRED_PROVIDER_CERT, 
                                e.getCertificate().getSubject(), null);
                    case CertificateException.ROOT_CA_EXPIRED:
                        throw new PaymentException(
                                PaymentException.EXPIRED_CA_CERT, 
                                e.getCertificate().getIssuer(), null);
                    default:
                        throw new PaymentException(
                                PaymentException.INVALID_PROVIDER_CERT, 
                                e.getCertificate().getSubject(), null);
                }
            }
            
            certIndex = 1;
            encodedCert = props.getProperty(PAY_CERTIFICATE_PREFIX +
                    ++certPath + "-" + certIndex);
        } while (encodedCert != null);
        
        throw new PaymentException(PaymentException.NO_TRUSTED_CHAIN);
    }
       
    /**
     * Strips all empty lines and lines containing any of 
     * <code>Pay-Certificate-*</code> or <code>Pay-Signature-*</code> 
     * attributes from the given string.
     *
     * @param string property containing string
     * @return the altered string
     */
    private static String removePKIProperties(String string) {
        char[] data = string.toCharArray();
        int length = data.length;
        StringBuffer buffer = new StringBuffer();
        
        int i = 0;
        int j, k;
        do {
            // skip empty lines
            for (j = i; (j < length) && (data[j] != '\n') && (data[j] <= ' ');
                ++j) {
            }
            if (j == length) {
                break;
            }
            if (data[j] == '\n') {
                i = j + 1;
                continue;
            }
            
            // find matching prefix
            int prefixIdx;
            for (prefixIdx = 0; prefixIdx < PKI_PREFIXES.length; ++prefixIdx) {
                char[] prefix = PKI_PREFIXES[prefixIdx];
                for (k = 0, j = i; (j < length) && (k < prefix.length) &&
                        (data[j] == prefix[k]); ++j, ++k) {
                }
                if (k == prefix.length) {
                    break;
                }
            }

            // find the end of the line
            for (j = i; (j < length) && (data[j] != '\n'); ++j) {
            }
            if (j < length) {
                // skip '\n'
                ++j;
            }
            
            // accept the lines that don't start with any of PKI_PREFIXES
            if (prefixIdx == PKI_PREFIXES.length) {
                buffer.append(data, i, j - i);
            }
            i = j;
        } while (i < length);
        
        return buffer.toString();
    }  
}