/* * * * Copyright 1990-2009 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 javax.microedition.payment.TransactionListener; import javax.microedition.payment.TransactionRecord; import javax.microedition.payment.TransactionModuleException; import com.sun.midp.security.*; import com.sun.midp.util.DateParser; import com.sun.midp.io.Util; import com.sun.midp.main.Configuration; import java.io.*; import java.util.Hashtable; import java.util.Vector; import javax.microedition.lcdui.*; import javax.microedition.io.Connector; import javax.microedition.io.Connection; import javax.microedition.io.HttpConnection; import javax.microedition.io.ConnectionNotFoundException; /** * This class represents a payment module. An instance of this class accepts * payment requests from transaction modules. For each payment request it * creates an instance of the <code>Transaction</code> class, set itself as * an initial transaction processor of this transaction and adds it to the * internal queue. * <p> * The internal queue is processed by a background thread created by the * instance of the payment module. This thread iterates through the queued * transactions and calls their <code>process</code> methods. This changes the * internal states of the transactions until they are fully processed and are * removed from the queue. * <p> * The <code>process</code> method of a transaction delegates the call to the * associated transaction processor. This is initially the payment module. That * allows him to show a provider selection form. After the user selects the * provider for the transaction, the payment module changes the transaction * processor of the transaction to the provider's payment adapter. This adapter * gets the control over the transaction till it finishes the transaction and * returns the control over it back to the payment module. The payment module * finally removes the transaction from the transaction queue and notifies the * application about the result of the payment. * <p> * The notification is done by creating a new transaction record for the * finished transaction and adding it to the transaction notification * queue which is processed by an extra thread which handles the notifications. * Each transaction record is automatically added to the transaction store, so * it can be obtained after crashing of the emulator. * * @version */ public abstract class PaymentModule implements TransactionProcessor { /** * Inner class to request security token from SecurityInitializer. * SecurityInitializer should be able to check this inner class name. */ static private class SecurityTrusted implements ImplicitlyTrustedClass {}; /** This class has a different security domain than the MIDlet suite. */ private static SecurityToken classSecurityToken = SecurityInitializer.requestToken(new SecurityTrusted()); /** Standard timeout for alerts. */ private static final int ALERT_TIMEOUT = 1250; /** The property that contains payment module class name. */ private static final String PAYMENT_MODULE = "microedition.payment.paymentmodule"; /** Payment module singleton. */ private static PaymentModule paymentModule; /** Transaction processing thread. */ private TransactionProcessingThread processingThread; /** Transaction state notification thread. */ private TransactionNotificationThread notificationThread; /** * Creates a new instance of PaymentModule. */ protected PaymentModule() { } /** * Returns an instance of the <code>PaymentModule</code> class. It creates * only one instance and reuses it each time the method is called. * * @return the instance */ public static PaymentModule getInstance() { if (paymentModule == null) { String className = Configuration.getProperty(PAYMENT_MODULE); if (className != null) { try { paymentModule = (PaymentModule) Class.forName(className).newInstance(); } catch (ClassNotFoundException cnfe) { // intentionally ignored } catch (InstantiationException ie) { // intentionally ignored } catch (IllegalAccessException iae) { // intentionally ignored } } // if (paymentModule == null) { // paymentModule = new PaymentModule(); // } } return paymentModule; } /** * It's a factory method for <code>TransactionModuleImpl</code>. * * @param object the application MIDlet initiating a payment transaction * @return a new instance of a <code>TransactionModuleImpl</code> subclass. * @throws TransactionModuleException indicates a creation failure */ public abstract TransactionModuleImpl createTransactionModule( Object object) throws TransactionModuleException; /** * Returns an instrance of the <code>TransactionStore</code> wich is used * for storing of all transaction records produced in the payment module. * There is only one such instance, which is returned each time the method * is called. This instance is used from both internal threads of the * payment module (<code>TransactionProcessingThread</code> and * <code>TransactionNotificationThread</code>) and it is left to an * implementation of the <code>TransactionStore</code> to be thread safe. * * @return the transaction store */ protected abstract TransactionStore getTransactionStore(); /** * Returns an instance of <code>Utils</code> subclass. * * @return the instance */ protected abstract Utils getUtilities(); /** * Returns a new application ID, which can be used to store transaction * records to the transaction store. * * @return the new application ID * @throws IOException if there is a I/O failure while working with the * store */ public final int getNextApplicationID() throws IOException { return getTransactionStore().getNextApplicationID(); } /** * Returns the associated payment information for the given transaction. * * @param transaction the transaction * @return the payment information */ protected static PaymentInfo getPaymentInfo(Transaction transaction) { return transaction.getTransactionModule().getPaymentInfo(); } /** * Saves the payment information for the given transaction into storage. * * @param transaction the transaction */ protected static void savePaymentInfo(Transaction transaction) { try { transaction.getTransactionModule().savePaymentInfo(); } catch (IOException e) { // ignore } } /** * This class represents a transaction processing thread. It iterates * through the queued transactions and executes the <code>process</code> * method on them. */ private class TransactionProcessingThread extends Thread { /** List of transactions being processing. */ private Transaction[] transactionQueue = new Transaction[16]; /** Processing thread exit flag. */ private boolean finished; /** Wait for next transaction flag. */ private boolean wait = true; /** * This method is run when the thread starts. It implements the * selection and execution of the queued transactions. */ public void run() { // transaction processing queue while (!finished) { boolean blockUI = false; for (int i = 0; i < transactionQueue.length; ++i) { Transaction transaction = transactionQueue[i]; if (transaction == null) { continue; } if (blockUI && transaction.needsUI()) { continue; } if (transaction.needsUI()) { blockUI = true; } if (transaction.isWaiting()) { continue; } transaction = transaction.process(); if ((transaction != null) && transaction.needsUI()) { blockUI = true; } transactionQueue[i] = transaction; wait = false; } synchronized (transactionQueue) { if (wait) { try { transactionQueue.wait(); } catch (InterruptedException e) { } } wait = true; } } } /** * Adds a transaction to the transaction queue and makes the * transaction processing thread to continue its work. * * @param transaction the transaction * @throws TransactionModuleException if there is no more space for the * new transaction in the queue */ public void addTransaction(Transaction transaction) throws TransactionModuleException { int i; for (i = 0; i < transactionQueue.length; ++i) { if (transactionQueue[i] == null) { transactionQueue[i] = transaction; // signal to the transaction processing thread, that there // is something to do continueWork(); break; } } if (i == transactionQueue.length) { throw new TransactionModuleException("No more space for " + "new transactions"); } } /** * Tells the transaction processing thread to continue processing the * transactions. */ public void continueWork() { synchronized (transactionQueue) { wait = false; transactionQueue.notify(); } } } /** * This class represents a transaction notification thread. It notifies * the transaction listeners associated with the transactions about the * final state of the transactions. After a successful notification it * clears the transaction record's "was missed" flag. * <p> * It gets the transactions for the notification from its internal * transaction notification queue and it has methods allowing the addition * of new transaction records to this queue. Each transaction in the queue * has associated a transaction module (<code>TransactionModuleImpl</code>) * which in turn holds a reference to the transaction listener, that should * be notified. */ private class TransactionNotificationThread extends Thread { /** Transaction listeners array */ private Vector notificationQueue = new Vector(); /** Notification thread exit flag. */ private boolean finished; /** Wait for next transaction flag. */ private boolean wait = true; /** * Offset of TransactionRecord object * inside notificationQueue element. */ private static final int RECORD = 0; /** * Offset of TransactionModuleImpl object * inside notificationQueue element. */ private static final int MODULE = 1; /** * This method is run when the thread starts. */ public void run() { TransactionStore transactionStore = getTransactionStore(); // listeners notification queue while (!finished) { int count = notificationQueue.size(); while (count > 0) { Object[] element = (Object[])notificationQueue.elementAt(0); // synchronized with the transaction module setListener // method => if the application invokes rhe // <code>TransactionModule.setListener</code> method with // <code>null</code>, after the call ends, there will be // no further notifications and no notification will be in // progress at that time synchronized (element[MODULE]) { TransactionRecord record = (TransactionRecord) element[RECORD]; TransactionListener listener = ((TransactionModuleImpl) element[MODULE]).getListener(); if (listener != null) { try { int transactionID = record.getTransactionID(); // test if the record has been delivered before, // because of for example 2 successive calls to // deliverMissedTransactions if (!transactionStore.wasDelivered( transactionID)) { listener.processed(record); transactionStore.setDelivered( transactionID); } } catch (IOException e) { // failed to notify or a transaction store // failure // ignore } } notificationQueue.removeElementAt(0); --count; } } synchronized (notificationQueue) { if (wait) { try { notificationQueue.wait(); } catch (InterruptedException e) { } } wait = true; } } } /** * Add the given transaction record and transaction module to the * transaction notification queue. It wakes up the transaction * notification thread if necessary. * * @param record the transaction record * @param module the transaction module */ public void addTransaction(TransactionRecord record, TransactionModuleImpl module) { Object[] element = new Object[] { record, module }; notificationQueue.addElement(element); continueWork(); } /** * Adds the given transaction records to the transaction notification * queue. Each transaction record is associated with the given * transaction module. If the transaction notification thread is not * running at the time the method is executed, it is woken up by the * method. * * @param records an array of the transaction records * @param module the module associated with the records */ public void addTransactions(TransactionRecord[] records, TransactionModuleImpl module) { Object[] element; for (int i = 0; i < records.length; ++i) { element = new Object[2]; element[RECORD] = records[i]; element[MODULE] = module; notificationQueue.addElement(element); } continueWork(); } /** * This method makes the transaction notification thread to continue * notification. */ public void continueWork() { synchronized (notificationQueue) { wait = false; notificationQueue.notify(); } } } /** * Signals the transaction processing thread to continue processing of the * queued transactions. It means that there is at least one transaction in * the queue, which is not waiting for some event (an user action, * finishing of some payment adapter's thread, etc.). */ final void continueProcessing() { processingThread.continueWork(); } /** * Creates a new transaction from the given payment requests. It sets the * payment module as a transaction processor and adds the new transaction * to the transaction queue. Returns a generated identification number, * which identifies the new transaction. * * @param transactionModule the transaction module, which called the method * @param featureID the identifier of the feature to be paid for * @param featureTitle the title of the feature * @param featureDescription the description of the feature * @param payload the payload to be transfered as a part of the payment or * <code>null</code> if no such payload required * @return the identification number of the transaction * @throws TransactionModuleException if there is no more space to store * the new transaction */ synchronized public final int addTransaction( TransactionModuleImpl transactionModule, int featureID, String featureTitle, String featureDescription, byte[] payload) throws TransactionModuleException { // execute the transaction processing thread if not running if (processingThread == null) { processingThread = new TransactionProcessingThread(); processingThread.start(); } TransactionStore transactionStore = getTransactionStore(); Transaction transaction = new Transaction(this, transactionModule, featureID, featureTitle, featureDescription, payload); int transactionID; try { transactionID = transactionStore.reserve( transactionModule.getApplicationID(), transaction); } catch (IOException e) { throw new TransactionModuleException("No more space for " + "transaction records"); } transaction.setTransactionID(transactionID); PaymentInfo paymentInfo = getPaymentInfo(transaction); synchronized (paymentInfo) { processingThread.addTransaction(transaction); if (paymentInfo.needsUpdate()) { try { paymentInfo.wait(); } catch (InterruptedException e) { // ignore } if (paymentInfo.needsUpdate()) { throw new TransactionModuleException("The provisioning " + "information needs an update"); } } } return transactionID; } /** * Adds the given transaction record to the transaction notification queue * and associates it with the given payment module. The payment module holds * a reference to the listener, which should be notified. * * @param record the transaction record * @param module the transaction module */ final void addTransactionForNotification(TransactionRecord record, TransactionModuleImpl module) { // execute the transaction notification thread if not running if (notificationThread == null) { notificationThread = new TransactionNotificationThread(); notificationThread.start(); } notificationThread.addTransaction(record, module); } /** * Adds the given transaction records to the transaction notification * queue and associates them with the given payment module. The payment * module holds a reference to the listener, which should be notified. * * @param records an array of the transaction records * @param module the transaction module */ final void addTransactionsForNotification(TransactionRecord[] records, TransactionModuleImpl module) { // execute the transaction notification thread if not running if (notificationThread == null) { notificationThread = new TransactionNotificationThread(); notificationThread.start(); } notificationThread.addTransactions(records, module); } /** Pointer to utility methods class. */ private final Utils utilities = getUtilities(); /** 'NEVER' string */ private final String NEVER = utilities.getString( Utils.PAYMENT_PROV_SEL_DLG_NEVER); /** * This class represents an UI for displaying and updating of payment * information and selecting a payment provider for the payment. The * payment is represented by an instance of the <code>Transaction</code> * class. */ private class PaymentModuleUI implements CommandListener, ItemCommandListener, ItemStateListener { /** A transaction which represents the payment. */ private Transaction transaction; /** Reject payment command. */ private final Command rejectCommand = new Command( utilities.getString(Utils.PAYMENT_PROV_SEL_DLG_NO), Command.CANCEL, 1); /** Accept payment command. */ private final Command acceptCommand = new Command( utilities.getString(Utils.PAYMENT_PROV_SEL_DLG_YES), Command.OK, 1); /** Update payment info command. */ private final Command updateCommand = new Command( utilities.getString(Utils.PAYMENT_PROV_SEL_DLG_UPDATE), Command.ITEM, 2); /** Cancel payment info update command. */ private final Command stopCommand = new Command( utilities.getString(Utils.PAYMENT_UPDATE_DLG_STOP), Command.STOP, 1); /** Provider selection form. */ private Form providerSelectionForm; /** Question string max length. */ private static final int QUESTION_LENGTH = 90; /** Feature description form item. */ private StringItem featureDescriptionItem; /** Payment question form item. */ private StringItem paymentQuestionItem; /** Provider selection choice group. */ private ChoiceGroup providerSelectionChoice; /** Payment info update date item. */ private StringItem updateDateItem; /** Last update stamp item. */ private StringItem updateStampItem; /** Provider identifiers array. */ private int[] providers; /** Payment info update form. */ private Form paymentUpdateForm; /** Payment info update progress gauge. */ private Gauge progressGauge; /** Payment info update state. */ private int updateState = -1; /** Flag indicates that payment info update was canceled. */ private boolean cancel; /** * Creates an instance of the <code>PaymentModuleUI</code> class. * It requires a transaction which represents the payment. * * @param transaction the transaction */ public PaymentModuleUI(Transaction transaction) { this.transaction = transaction; } /** Displays a payment update form for the transaction. */ public void showPaymentUpdateForm() { if (paymentUpdateForm == null) { // create the form paymentUpdateForm = new Form(utilities.getString( Utils.PAYMENT_UPDATE_DLG_CAPTION)); progressGauge = new Gauge(null, false, Gauge.INDEFINITE, Gauge.CONTINUOUS_RUNNING); progressGauge.setPreferredSize(paymentUpdateForm.getWidth(), -1); paymentUpdateForm.append(progressGauge); paymentUpdateForm.addCommand(stopCommand); paymentUpdateForm.setCommandListener(this); } updatePaymentUpdateForm(); preemptDisplay(classSecurityToken, paymentUpdateForm); } /** Displays a provider selection form for the transaction. */ public void showProviderSelectionForm() { if (providerSelectionForm == null) { // create the form providerSelectionForm = new Form(transaction.getFeatureTitle()); featureDescriptionItem = new StringItem( transaction.getFeatureDescription(), null); paymentQuestionItem = new StringItem(null, null); providerSelectionChoice = new ChoiceGroup( utilities.getString(Utils.PAYMENT_PROV_SEL_DLG_PAY_BY), ChoiceGroup.POPUP); updateDateItem = new StringItem(utilities.getString( Utils.PAYMENT_PROV_SEL_DLG_UPDATE_DATE), null); updateStampItem = new StringItem(utilities.getString( Utils.PAYMENT_PROV_SEL_DLG_UPDATE_STAMP), null); featureDescriptionItem.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); paymentQuestionItem.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); providerSelectionChoice.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); updateDateItem.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); updateStampItem.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); Font defaultFont = Font.getDefaultFont(); StringItem separator; int separatorHeight = defaultFont.getHeight() >>> 1; int separatorWidth = providerSelectionForm.getWidth() >>> 1; int reserveLines = defaultFont.charWidth('M') * QUESTION_LENGTH / providerSelectionForm.getWidth() + 1; paymentQuestionItem.setPreferredSize(-1, reserveLines * defaultFont.getHeight()); separator = new StringItem(null, null); separator.setPreferredSize(separatorWidth, separatorHeight); separator.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); providerSelectionForm.append(featureDescriptionItem); providerSelectionForm.append(separator); providerSelectionForm.append(paymentQuestionItem); providerSelectionForm.append(providerSelectionChoice); separator = new StringItem(null, null); separator.setPreferredSize(separatorWidth, separatorHeight); separator.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); providerSelectionForm.append(separator); providerSelectionForm.append(updateDateItem); providerSelectionForm.append(updateStampItem); StringItem updateItem = new StringItem(null, updateCommand.getLabel(), Item.BUTTON); updateItem.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_RIGHT); updateItem.setDefaultCommand(updateCommand); updateItem.setItemCommandListener(this); separator = new StringItem(null, null); separator.setPreferredSize(separatorWidth, separatorHeight); separator.setLayout(Item.LAYOUT_NEWLINE_BEFORE | Item.LAYOUT_NEWLINE_AFTER); providerSelectionForm.append(separator); providerSelectionForm.append(updateItem); // reset the layout to the left StringItem lastItem = new StringItem(null, null); lastItem.setLayout(Item.LAYOUT_LEFT); providerSelectionForm.append(lastItem); providerSelectionForm.addCommand(acceptCommand); providerSelectionForm.addCommand(rejectCommand); providerSelectionForm.setCommandListener(this); providerSelectionForm.setItemStateListener(this); } PaymentInfo paymentInfo = getPaymentInfo(transaction); providers = getValidProviders(paymentInfo); // fill the provider selection choice group int oldIndex = providerSelectionChoice.getSelectedIndex(); providerSelectionChoice.deleteAll(); for (int i = 0; i < providers.length; ++i) { ProviderInfo providerInfo = paymentInfo.getProvider( providers[i]); PaymentAdapter adapter = null; try { adapter = getAdapter(providerInfo.getAdapter(), providerInfo.getConfiguration()); } catch (PaymentException e) { } providerSelectionChoice.append(adapter.getDisplayName() + " - " + providerInfo.getName(), null); } if (oldIndex >= providers.length) { oldIndex = providers.length - 1; } if (oldIndex < 0) { oldIndex = 0; } providerSelectionChoice.setSelectedIndex(oldIndex, true); updateProviderSelectionForm(); preemptDisplay(classSecurityToken, providerSelectionForm); } /** * Displays an alert with the given title and message. * * @param title the title * @param message the message */ private void displayException(String title, String message) { Alert a = new Alert(title, message, null, AlertType.ERROR); a.setTimeout(Alert.FOREVER); a.setCommandListener(this); preemptDisplay(classSecurityToken, a); } /** * Updates the user interface of the provider selection form to reflect * changes made by the user. */ private void updateProviderSelectionForm() { int providerID = providers[ providerSelectionChoice.getSelectedIndex()]; PaymentInfo paymentInfo = getPaymentInfo(transaction); int priceTag = paymentInfo.getPriceTagForFeature( transaction.getFeatureID()); updateDateItem.setText((paymentInfo.getUpdateDate() == null) ? NEVER : paymentInfo.getUpdateDate().toString()); updateStampItem.setText(paymentInfo.getUpdateStamp().toString()); ProviderInfo providerInfo = paymentInfo.getProvider(providerID); PaymentAdapter adapter = null; try { adapter = getAdapter(providerInfo.getAdapter(), providerInfo.getConfiguration()); } catch (PaymentException e) { } String question = adapter.getPaymentQuestion(providerInfo.getName(), providerInfo.getPrice(priceTag), providerInfo.getCurrency()); paymentQuestionItem.setText(question); } /** * Updates the payment update form to reflect the state the payment * update is in. */ private void updatePaymentUpdateForm() { int key; int retry = 0; if (updateState == -1) { progressGauge.setLabel(null); return; } if (cancel) { progressGauge.setLabel(utilities.getString( Utils.PAYMENT_UPDATE_DLG_CANCELLING)); return; } if (updateState < STATE_DOWNLOADING) { retry = updateState >>> RETRY_SHIFT; updateState &= (1 << RETRY_SHIFT) - 1; } switch (updateState) { case STATE_CONNECTING: key = Utils.PAYMENT_UPDATE_DLG_CONNECTING; break; case STATE_SENDING_REQUEST: key = Utils.PAYMENT_UPDATE_DLG_SENDING; break; case STATE_RETRY_WAITING: key = Utils.PAYMENT_UPDATE_DLG_WAITING; break; case STATE_DOWNLOADING: key = Utils.PAYMENT_UPDATE_DLG_DOWNLOADING; break; case STATE_VERIFYING: key = Utils.PAYMENT_UPDATE_DLG_VERIFYING; break; default: return; } String message = utilities.getString(key); if (retry > 0) { String[] params = { Integer.toString(retry), Integer.toString(MAX_RETRY_COUNT) }; message += "\n" + utilities.getString( Utils.PAYMENT_UPDATE_DLG_RETRY, params); } progressGauge.setLabel(message); } /** Time of payment update form last update. */ private long lastUIUpdate; /** * A method which is called by the payment module when the state of the * payment update changes. * * @param newState the new state of the payment update * @throws InterruptedException if the payment update has been * interrupted by the user */ public void notifyStateChange(int newState) throws InterruptedException { if (cancel) { throw new InterruptedException("stopped"); } if (updateState != newState) { long sleepTime = lastUIUpdate + ALERT_TIMEOUT - System.currentTimeMillis(); if (sleepTime > 0) { Thread.sleep(sleepTime); } updateState = newState; updatePaymentUpdateForm(); lastUIUpdate = System.currentTimeMillis(); } } /** * Implements a response to user actions. * * @param c the executed command * @param d the <code>Displayable</code> on which the command has * been executed */ public void commandAction(Command c, Displayable d) { if (c == acceptCommand) { preemptDisplay(classSecurityToken, null); currentUI = null; assignTransaction(transaction, providers[ providerSelectionChoice.getSelectedIndex()]); transaction.setWaiting(false); } else if (c == rejectCommand) { preemptDisplay(classSecurityToken, null); currentUI = null; // reject the transaction transaction.setState(Transaction.REJECTED); transaction.setNeedsUI(false); transaction.setWaiting(false); } else if (c == Alert.DISMISS_COMMAND) { preemptDisplay(classSecurityToken, null); PaymentInfo paymentInfo = getPaymentInfo(transaction); if (paymentInfo.needsUpdate()) { // we failed to update and the current information can't // be used => fail the transaction currentUI = null; // discard the transaction transaction.setState(Transaction.DISCARDED); transaction.setNeedsUI(false); transaction.setWaiting(false); } else { transaction.setState(Transaction.ENTERED); transaction.setWaiting(false); // showProviderSelectionForm(); } // release the process method if waiting for an update synchronized (paymentInfo) { paymentInfo.notifyAll(); } } else if (c == stopCommand) { if (!cancel) { cancel = true; updatePaymentUpdateForm(); // processingThread.interrupt(); } } } /** * Implements a response to user actions. * * @param c the executed command * @param item the item associated with the executed command */ public void commandAction(Command c, Item item) { if (c == updateCommand) { preemptDisplay(classSecurityToken, null); updateState = -1; cancel = false; transaction.setState(Transaction.UPDATE); transaction.setWaiting(false); } } /** * Called when internal state of an Item has been changed by the user. * * @param item the item that was changed */ public void itemStateChanged(Item item) { if (item == providerSelectionChoice) { updateProviderSelectionForm(); } } } /** * Assigns the given transaction to the provider identified by its * identification number. It sets the transaction processor of the * transaction to the provider's payment adapter. * * @param transaction the transaction * @param providerID the provider id */ protected void assignTransaction(Transaction transaction, int providerID) { PaymentInfo paymentInfo = getPaymentInfo(transaction); int priceTag = paymentInfo.getPriceTagForFeature( transaction.getFeatureID()); ProviderInfo providerInfo = paymentInfo.getProvider(providerID); // get the adapter instance for the given provider PaymentAdapter adapter = null; try { adapter = getAdapter(providerInfo.getAdapter(), providerInfo.getConfiguration()); } catch (PaymentException e) { } // fill the transaction fields with the provider specific values transaction.setProviderName(providerInfo.getName()); transaction.setCurrency(providerInfo.getCurrency()); transaction.setPrice(providerInfo.getPrice(priceTag)); transaction.setSpecificPriceInfo( providerInfo.getPaySpecificPriceInfo(priceTag)); // === DEBUG MODE === // let a subclass to handle the transaction in the debug mode, if it // does, don't forward the control over the transaction to the payment // adapter if (handleTransactionDebugMode(transaction)) { return; } // === DEBUG MODE === // set the adapter to be a transaction processor for the transaction transaction.setTransactionProcessor(adapter); // update the state of the transaction transaction.setState(Transaction.ASSIGNED); } /** * Returns an array of provider identifiers, which could be used to pay * for features. * * @param paymentInfo the payment information for the MIDlet which * initiated the payment * @return the array of provider identifiers */ protected final int[] getValidProviders(PaymentInfo paymentInfo) { int numProviders = paymentInfo.getNumProviders(); int numAccepted = 0; boolean[] accepted = new boolean[numProviders]; for (int i = 0; i < numProviders; ++i) { accepted[i] = false; ProviderInfo providerInfo = paymentInfo.getProvider(i); PaymentAdapter adapter = null; try { adapter = getAdapter(providerInfo.getAdapter(), providerInfo.getConfiguration()); } catch (PaymentException e) { } if (adapter == null) { continue; } accepted[i] = true; ++numAccepted; } int[] providers = new int[numAccepted]; for (int i = 0, j = 0; i < numProviders; ++i) { if (accepted[i]) { providers[j++] = i; } } return providers; } /** Array of created payment adapters. */ private Hashtable paymentAdapters = new Hashtable(); /** * Indicates if the given adapter is supported by the device. * * @param name the name of the adapter * @return <code>true</code> if the given adapter is supported */ public boolean isSupportedAdapter(String name) { if ("PPSMS".equals(name)) { return true; } return false; } /** * Creates the payment adapter for the given registered adapter name and * the adapter configuration string. It returns <code>null</code> if no * such adapter can be created. * * @param adapter the registered adapter name * @param configuration the adapter configuration string * @return the instance of the payment adapter or <code>null</code> * @throws PaymentException if the adapter configuration string * has an invalid format */ protected PaymentAdapter createAdapter(String adapter, String configuration) throws PaymentException { if ("PPSMS".equals(adapter)) { if (configuration.indexOf(',') != -1) { String mcc = configuration.substring(0, configuration.indexOf(',')); String mnc = configuration.substring(configuration.indexOf(',') + 1); if ((mcc != null) && (mnc != null)) { mcc = mcc.trim(); mnc = mnc.trim(); try { Integer.parseInt(mcc); Integer.parseInt(mnc); } catch (NumberFormatException nfe) { throw new PaymentException( PaymentException.INVALID_ADAPTER_CONFIGURATION, configuration); } if ((mcc.length() != 3) || (mnc.length() < 2) || (mnc.length() > 3)) { throw new PaymentException( PaymentException.INVALID_ADAPTER_CONFIGURATION, configuration); } // get system property String MCC = System.getProperty("MCC"); // get jsr default property if (null == MCC) { MCC = System.getProperty("payment.mcc"); } // get system property String MNC = System.getProperty("MNC"); // get jsr default property if (null == MNC) { MNC = System.getProperty("payment.mnc"); } if (mcc.equals(MCC) && mnc.equals(MNC)) { return PPSMSAdapter.getInstance(configuration); } else { return null; } } else { throw new PaymentException( PaymentException.INVALID_ADAPTER_CONFIGURATION, configuration); } } else { throw new PaymentException( PaymentException.INVALID_ADAPTER_CONFIGURATION, configuration); } } return null; } /** * Returns the payment adapter for the given registered adapter name and * the adapter configuration string. It either returns an adapter created * before for the given combination of <code>name</code> and * <code>providerString</code> or creates a new instance if the old one * doesn't exist. It returns <code>null</code> if no adapter of the given * parameters can be created. * * @param name the registered adapter name * @param configuration the adapter configuration string * @return the instance of the payment adapter or <code>null</code> * @throws PaymentException if the adapter configuration string * has an invalid format */ PaymentAdapter getAdapter(String name, String configuration) throws PaymentException { String adapterLookupString = name + "#" + normalizeConfigurationString(configuration); PaymentAdapter adapter = (PaymentAdapter)paymentAdapters.get( adapterLookupString); if (adapter == null) { adapter = createAdapter(name, configuration); if (adapter != null) { paymentAdapters.put(adapterLookupString, adapter); } } return adapter; } /** * Replaces the current <code>Displayable</code> with the new one if the * <code>nextDisplayable</code> is not <code>null</code> or it recovers * the previous <code>Displayable</code> if the <code>nextDisplayable</code> * is <code>null</code>. * * @param token a security token, which allows preempting * @param nextDisplayable the <code>Displayable</code> to show or * <code>null</code> if the recovery of the old * <code>Displayable</code> is requested */ protected abstract void preemptDisplay(SecurityToken token, Displayable nextDisplayable); /** Current payment module UI. */ private PaymentModuleUI currentUI; /** * A method which is responsible for processing of transactions which are * not yet assigned to the provider specific adapters or they are finished * and need to be removed from the transaction processing queue. * * @param transaction the transaction to be processed * @return the processed transaction or <code>null</code> if the transaction * should be removed from the transaction queue */ public Transaction process(Transaction transaction) { PaymentInfo paymentInfo; boolean needsUpdate; switch (transaction.getState()) { case Transaction.ENTERED: paymentInfo = getPaymentInfo(transaction); needsUpdate = paymentInfo.needsUpdate(); // === DEBUG MODE === if (!needsUpdate && handleAutoRequestMode(transaction)) { break; } // === DEBUG MODE === currentUI = new PaymentModuleUI(transaction); if (needsUpdate) { transaction.setState(Transaction.UPDATE); } else { transaction.setWaiting(true); currentUI.showProviderSelectionForm(); } break; case Transaction.UPDATE: // currentUI != null paymentInfo = getPaymentInfo(transaction); // validate the http or https connection now (before we preempt // the display) try { String name = paymentInfo.getUpdateURL(); String permission = "javax.microedition.io.Connector.http"; if (name.startsWith("https")) permission += "s"; int colon = name.indexOf(':'); if (colon != -1) { if (colon < name.length() - 1) { name = name.substring(colon + 1); } else { name = ""; } } transaction.getTransactionModule().checkForPermission( permission, name); } catch (InterruptedException e) { // ignore, let the download fail } transaction.setWaiting(true); currentUI.showPaymentUpdateForm(); Exception ex = null; try { synchronized (paymentInfo) { updatePaymentInfo(paymentInfo); } if (paymentInfo.cache()) { savePaymentInfo(transaction); } preemptDisplay(classSecurityToken, null); // === DEBUG MODE === if (handleAutoRequestMode(transaction)) { transaction.setWaiting(false); currentUI = null; break; } // === DEBUG MODE === currentUI.showProviderSelectionForm(); } catch (InterruptedException e) { preemptDisplay(classSecurityToken, null); if (paymentInfo.needsUpdate()) { currentUI = null; // discard the transaction transaction.setState(Transaction.DISCARDED); transaction.setNeedsUI(false); transaction.setWaiting(false); } else { currentUI.showProviderSelectionForm(); } } catch (PaymentException pe) { ex = pe; } catch (IOException ioe) { ex = ioe; } if (ex != null) { preemptDisplay(classSecurityToken, null); currentUI.displayException(utilities.getString( Utils.PAYMENT_ERROR_DLG_CAPTION), getErrorMessage(ex)); // don't release the process method if waiting for an // update yet break; } // release the process method if waiting for an update synchronized (paymentInfo) { paymentInfo.notifyAll(); } break; case Transaction.REJECTED: case Transaction.SUCCESSFUL: case Transaction.FAILED: TransactionStore transactionStore = getTransactionStore(); try { // create a transaction record for the transaction and add // it to the transaction notification queue TransactionRecord transactionRecord = transactionStore.addTransaction(transaction); addTransactionForNotification(transactionRecord, transaction.getTransactionModule()); } catch (IOException e) { } // fall through case Transaction.DISCARDED: return null; } return transaction; } /** Number of payment update file download attempts. */ private static final int MAX_RETRY_COUNT = 3; /** UI update constant. */ private static final int RETRY_SHIFT = 2; /** Payment info update stage. */ private static final int STATE_CONNECTING = 0; /** Payment info update stage. */ private static final int STATE_SENDING_REQUEST = 1; /** Payment info update stage. */ private static final int STATE_RETRY_WAITING = 2; /** Payment info update stage. */ private static final int STATE_DOWNLOADING = 0x100; /** Payment info update stage. */ private static final int STATE_VERIFYING = 0x101; /** Payment info update stage. */ private static final int STATE_FINISHED = 0x200; /** Index of MIME type object inside content type array. */ private static final int MIME_TYPE = 0; /** Index of CHARSET type object inside content type array. */ private static final int CHARSET = 1; /** Maximum size of payment update file. */ private static final int TRANSFER_CHUNK = 1024; /** Payment info update file MIME type. */ private static final String UPDATE_MIME_TYPE = "text/vnd.sun.pay.provision"; /** * Returns an error message for the given exception. It handles exceptions * thrown during a payment update. * * @param e the exception * @return the error message */ private String getErrorMessage(Exception e) { int prefixKey = Utils.PAYMENT_ERROR_PREFIX; int suffixKey = Utils.PAYMENT_ERROR_SUFFIX; int key = -1; if (e instanceof SecurityException) { key = Utils.PAYMENT_ERROR_PERMISSIONS; suffixKey = -1; } else if (e instanceof IOException) { key = Utils.PAYMENT_ERROR_DOWNLOAD_FAILED; suffixKey = -1; } else if (e instanceof PaymentException) { PaymentException pe = (PaymentException)e; switch (pe.getReason()) { case PaymentException.UNSUPPORTED_PAYMENT_INFO: case PaymentException.UNSUPPORTED_ADAPTERS: case PaymentException.UNSUPPORTED_PROVIDERS: case PaymentException.UNSUPPORTED_URL_SCHEME: case PaymentException.UNSUPPORTED_UPDATE_CHARSET: key = Utils.PAYMENT_ERROR_UPDATE_NOT_SUPPORTED; break; case PaymentException.INFORMATION_NOT_YET_VALID: key = Utils.PAYMENT_ERROR_UPDATE_NOT_YET_VALID; break; case PaymentException.INFORMATION_EXPIRED: key = Utils.PAYMENT_ERROR_UPDATE_EXPIRED; break; case PaymentException.MISSING_MANDATORY_ATTRIBUTE: case PaymentException.INVALID_ATTRIBUTE_VALUE: case PaymentException.INVALID_ADAPTER_CONFIGURATION: case PaymentException.INVALID_PRICE_INFORMATION: case PaymentException.INVALID_PROPERTIES_FORMAT: key = Utils.PAYMENT_ERROR_UPDATE_INVALID; break; case PaymentException.INCOMPLETE_INFORMATION: key = Utils.PAYMENT_ERROR_UPDATE_INCOMPLETE; break; case PaymentException.UPDATE_SERVER_NOT_FOUND: case PaymentException.UPDATE_NOT_FOUND: case PaymentException.INVALID_UPDATE_URL: key = Utils.PAYMENT_ERROR_UPDATE_NOT_FOUND; break; case PaymentException.UPDATE_SERVER_BUSY: case PaymentException.UPDATE_REQUEST_ERROR: key = Utils.PAYMENT_ERROR_CONNECTION_FAILED; break; case PaymentException.INVALID_UPDATE_TYPE: key = Utils.PAYMENT_ERROR_UPDATE_INVALID_TYPE; break; case PaymentException.EXPIRED_PROVIDER_CERT: key = Utils.PAYMENT_ERROR_CERTIFICATE_EXPIRED; break; case PaymentException.INVALID_PROVIDER_CERT: key = Utils.PAYMENT_ERROR_CERTIFICATE_INCORRECT; break; case PaymentException.EXPIRED_CA_CERT: case PaymentException.NO_TRUSTED_CHAIN: key = Utils.PAYMENT_ERROR_CERTIFICATE_UNTRUSTED; break; case PaymentException.SIGNATURE_VERIFICATION_FAILED: key = Utils.PAYMENT_ERROR_VERIFICATION_FAILED; break; } } StringBuffer buffer = new StringBuffer(utilities.getString(prefixKey)); if (key != -1) { buffer.append(" "); buffer.append(utilities.getString(key)); if (suffixKey != -1) { buffer.append(" "); buffer.append(utilities.getString(suffixKey)); } } return buffer.toString(); } /** * Creates a http or https connection with the payment update server. After * opening the connection it sends the http request for the update file and * checks the reply. If everything is correct it returns the connection * which can be used to get the update file. * * @param url the URL of the payment update file * @return the opened connection * @throws PaymentException indicates failure * @throws IOException indicates failure * @throws InterruptedException indicates that the thread has been * interrupted while waiting for the server */ private HttpConnection createConnection(String url) throws PaymentException, IOException, InterruptedException { HttpConnection httpConnection = null; int responseCode = -1; try { int retry = 0; do { currentUI.notifyStateChange(STATE_CONNECTING + (retry << RETRY_SHIFT)); Connection connection; try { connection = Connector.open(url); } catch (IllegalArgumentException e) { throw new PaymentException( PaymentException.INVALID_UPDATE_URL, url, null); } catch (ConnectionNotFoundException e) { throw new PaymentException( PaymentException.INVALID_UPDATE_URL, url, null); } if (!(connection instanceof HttpConnection)) { connection.close(); throw new PaymentException( PaymentException.INVALID_UPDATE_URL, url, null); } httpConnection = (HttpConnection)connection; // set User-Agent String prof = System.getProperty("microedition.profiles"); int space = prof.indexOf(' '); if (space != -1) { prof = prof.substring(0, space); } httpConnection.setRequestProperty("User-Agent", "Profile/" + prof + " Configuration/" + System.getProperty("microedition.configuration")); // set Accept-Charset httpConnection.setRequestProperty("Accept-Charset", "UTF-8, " + System.getProperty("microedition.encoding")); // set Accept-Language String locale = System.getProperty("microedition.locale"); if (locale != null) { httpConnection.setRequestProperty("Accept-Language", locale); } currentUI.notifyStateChange(STATE_SENDING_REQUEST + (retry << RETRY_SHIFT)); try { responseCode = httpConnection.getResponseCode(); } catch (IOException e) { if (httpConnection.getHost() == null) { throw new PaymentException( PaymentException.INVALID_UPDATE_URL, url, null); } throw new PaymentException( PaymentException.UPDATE_SERVER_NOT_FOUND, url, null); } if ((responseCode != HttpConnection.HTTP_UNAVAILABLE) || (++retry > MAX_RETRY_COUNT)) { break; } long sleepTime = 10000; String value = httpConnection.getHeaderField("Retry-After"); // parse the Retry-After field if (value != null) { try { sleepTime = Integer.parseInt(value) * 1000; } catch (NumberFormatException ne) { // not a number try { sleepTime = DateParser.parse(value); sleepTime -= System.currentTimeMillis(); } catch (IllegalArgumentException de) { } } } httpConnection.close(); httpConnection = null; if (sleepTime < 0) { sleepTime = 10000; } else if (sleepTime > 60000) { sleepTime = 60000; } currentUI.notifyStateChange(STATE_RETRY_WAITING + (retry << RETRY_SHIFT)); Thread.sleep(sleepTime); } while (true); switch (responseCode) { case HttpConnection.HTTP_OK: break; case HttpConnection.HTTP_NOT_FOUND: throw new PaymentException( PaymentException.UPDATE_NOT_FOUND, url, null); case HttpConnection.HTTP_UNAVAILABLE: throw new PaymentException( PaymentException.UPDATE_SERVER_BUSY, url, null); default: throw new PaymentException( PaymentException.UPDATE_REQUEST_ERROR, Integer.toString(responseCode), null); } } catch (PaymentException e) { if (httpConnection != null) { httpConnection.close(); } // rethrow throw e; } catch (IOException e) { if (httpConnection != null) { httpConnection.close(); } // rethrow throw e; } return httpConnection; } /** * Updates the given payment information from the update URL. If an * exception is thrown during the update, the payment information is not * changed. * * @param paymentInfo the payment information * @throws PaymentException indicates failure * @throws IOException indicates failure * @throws InterruptedException if the update has been stopped by the user */ private void updatePaymentInfo(PaymentInfo paymentInfo) throws PaymentException, IOException, InterruptedException { String url = paymentInfo.getUpdateURL(); // 1. CONNECT TO THE SERVER AND SEND REQUEST HttpConnection httpConnection = createConnection(url); String[] contentType = { null, null }; parseContentType(contentType, httpConnection.getType()); // check the mime type if (!UPDATE_MIME_TYPE.equals(contentType[MIME_TYPE])) { httpConnection.close(); throw new PaymentException(PaymentException.INVALID_UPDATE_TYPE, contentType[MIME_TYPE], null); } // 2. DOWNLOAD THE UPDATE FILE byte[] data = null; InputStream is; try { currentUI.notifyStateChange(STATE_DOWNLOADING); is = httpConnection.openDataInputStream(); try { ByteArrayOutputStream os = new ByteArrayOutputStream( TRANSFER_CHUNK); byte[] buffer = new byte[TRANSFER_CHUNK]; int read; while ((read = is.read(buffer, 0, TRANSFER_CHUNK)) > 0) { os.write(buffer, 0, read); } // not necessary os.flush(); data = os.toByteArray(); os.close(); } finally { is.close(); } } finally { httpConnection.close(); } // 3. VERIFY AND UPDATE PAYMENT INFORMATION currentUI.notifyStateChange(STATE_VERIFYING); paymentInfo.updatePaymentInfo(data, contentType[CHARSET]); currentUI.notifyStateChange(STATE_FINISHED); } /** * Returns the end index of the first substring of the given string which * matches the given mask or <code>-1</code> if no such substring can be * found. The mask is given as a character array and it has only one * special character '+', which represents 0..* number of space characters. * * @param string the string * @param mask the mask * @return the index or <code>-1</code> */ private static int findEndOf(String string, char[] mask) { char[] data = string.toCharArray(); int i = 0; // index into data int j = 0; // index into mask int k = 0; // saved index while ((i < data.length) && (j < mask.length)) { if (mask[j] == '+') { if (data[i] <= ' ') { ++i; } else { ++j; } continue; } if (data[i] == mask[j]) { ++i; ++j; continue; } i = k + 1; j = 0; k = i; } for (; (j < mask.length) && (mask[j] == '+'); ++j) { } return (j == mask.length) ? i : -1; } /** Mask for content type searching. */ private static final char[] CHARSET_MASK = { ';', '+', 'c', 'h', 'a', 'r', 's', 'e', 't', '+', '=' }; /** * Extracts the MIME type and the character set from the given Content-Type * value. It returns the extracted values in the given string array. They * are stored under the <code>MIME_TYPE</code> and <code>CHARSET</code> * indexes. * * @param values the array for extracted values * @param contentType the Content-Type value */ private static void parseContentType(String[] values, String contentType) { int index; String mimeType = null; String charset = "ISO-8859-1"; // the default charset // decode mimeType and charset if (contentType != null) { index = contentType.indexOf(';'); mimeType = contentType; if (index != -1) { mimeType = contentType.substring(0, index); index = findEndOf(contentType, CHARSET_MASK); if (index != -1) { int index2 = contentType.indexOf(';', index); if (index2 == -1) { index2 = contentType.length(); } charset = contentType.substring(index, index2); charset = charset.trim().toUpperCase(); } } mimeType = mimeType.trim().toLowerCase(); } values[MIME_TYPE] = mimeType; values[CHARSET] = charset; } /** * Normalizes the given configuration strings. It removes all white spaces * before and after any comma in the string. * * @param configuration the input string * @return the normalized string */ private static String normalizeConfigurationString(String configuration) { StringBuffer reconstructed = new StringBuffer(); Vector elements = Util.getCommaSeparatedValues(configuration); int count = elements.size(); if (count > 0) { reconstructed.append((String)elements.elementAt(0)); for (int i = 1; i < count; ++i) { reconstructed.append(","); reconstructed.append((String)elements.elementAt(i)); } } return reconstructed.toString(); } // === DEBUG MODE === /** * Handles the success/failure/random debug mode for the given transaction. * It's called from the parts of the <code>PaymentModule</code> code where * this mode can be applied. If the debug mode is in effect the transaction * state is set accordingly and the method returns <code>true</code>. * * @param transaction the transaction * @return <code>true</code> if the transaction is handled in the method */ protected boolean handleTransactionDebugMode(Transaction transaction) { return false; } /** * Handles the auto request debug mode for the given transaction. It's * called from the parts of the <code>PaymentModule</code> code where this * mode can be applied. If the auto request mode is in effect the * transaction state is set accordingly and the method returns * <code>true</code>. * * @param transaction the transaction * @return <code>true</code> if the transaction is handled in the method * (= the auto request mode is in effect) */ protected boolean handleAutoRequestMode(Transaction transaction) { return false; } // === DEBUG MODE === static { /* Hand out security token */ PPSMSAdapter.initSecurityToken(classSecurityToken); } }