/*
*
*
* 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();
}
}
|