001: /*
002: *
003: *
004: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: */
026:
027: package com.sun.midp.installer;
028:
029: import java.io.IOException;
030: import java.io.OutputStream;
031:
032: import javax.microedition.io.Connector;
033: import javax.microedition.io.HttpConnection;
034:
035: import com.sun.midp.configurator.Constants;
036:
037: import com.sun.midp.io.Base64;
038:
039: import com.sun.midp.io.j2me.http.Protocol;
040:
041: import com.sun.midp.midlet.MIDletSuite;
042:
043: import com.sun.midp.midlet.MIDletStateHandler;
044:
045: import com.sun.midp.midletsuite.MIDletSuiteStorage;
046:
047: import com.sun.midp.security.Permissions;
048: import com.sun.midp.security.SecurityToken;
049:
050: /**
051: * This class handles sending installation and deletion notifications as
052: * specified by the OTA section of the MIDP 2.0 specification.
053: * The delete notifications are only sent when an install notification is
054: * sent. The installer will call this class to send the initial install
055: * notification, if the notification fails and is a success notification,
056: * the notification will be queued, so that the next time a MIDlet from the
057: * suite is run, that suite's notification will be retried. The MIDlet
058: * state handler will call this class to process install notification retries.
059: */
060: public final class OtaNotifier {
061: /** Retry delay. */
062: static final int RETRY_DELAY = 3000; // 3 seconds
063:
064: /** MIDlet property for the install notify URL. */
065: public static final String NOTIFY_PROP = "MIDlet-Install-Notify";
066:
067: /** Success message for the suite provider. */
068: public static final String SUCCESS_MSG = "900 Success";
069:
070: /** Error message for the suite provider. */
071: public static final String INSUFFICIENT_MEM_MSG = "901 Insufficient Memory";
072:
073: /** Error message for the suite provider. */
074: public static final String USER_CANCELLED_MSG = "902 User Cancelled";
075:
076: /** Error message for the suite provider. */
077: public static final String JAR_SIZE_MISMATCH_MSG = "904 JAR size mismatch";
078:
079: /** Error message for the suite provider. */
080: public static final String ATTRIBUTE_MISMATCH_MSG = "905 Attribute Mismatch";
081:
082: /** Error message for the suite provider. */
083: public static final String INVALID_JAD_MSG = "906 Invalid Descriptor";
084:
085: /** Error message for the suite provider. */
086: public static final String INVALID_JAR_MSG = "907 Invalid JAR";
087:
088: /** Error message for the suite provider. */
089: public static final String INCOMPATIBLE_MSG = "908 Incompatible Configuration or Profile";
090:
091: /** Error message for authentication failure. */
092: public static final String AUTHENTICATION_FAILURE_MSG = "909 Application authentication failure";
093:
094: /** Error message for authorization failure. */
095: public static final String AUTHORIZATION_FAILURE_MSG = "910 Application authorization failure";
096:
097: /** Error message for push registration failure. */
098: public static final String PUSH_REG_FAILURE_MSG = "911 Push registration failure";
099:
100: /** Error message for push registration failure. */
101: public static final String DELETE_NOTIFICATION_MSG = "912 Deletion Notification";
102:
103: /** Message to send when a content handler install fails. */
104: public static final String CONTENT_HANDLER_CONFLICT = "938 Content handler conflicts with other handlers";
105:
106: /** Message to send when a content handler install fails. */
107: public static final String INVALID_CONTENT_HANDLER = "939 Content handler install failed";
108:
109: /**
110: * Posts a status message back to the provider's URL in JAD.
111: * This method will also retry ALL pending delete notifications.
112: *
113: * @param message status message to post
114: * @param suite MIDlet suite object
115: * @param proxyUsername if not null, it will be put in the post
116: * @param proxyPassword if not null, it will be put in the post
117: */
118: public static void postInstallMsgBackToProvider(String message,
119: MIDletSuite suite, String proxyUsername,
120: String proxyPassword) {
121: String url;
122: MIDletSuite callingMidletSuite = MIDletStateHandler
123: .getMidletStateHandler().getMIDletSuite();
124:
125: if (callingMidletSuite == null) {
126: throw new IllegalStateException(
127: "This method can't be called "
128: + "before a suite is started.");
129: }
130:
131: callingMidletSuite.checkIfPermissionAllowed(Permissions.AMS);
132:
133: // Now, send out install notifications
134: url = suite.getProperty(NOTIFY_PROP);
135: try {
136: postMsgBackToProvider(message, url, proxyUsername,
137: proxyPassword);
138: } catch (Throwable t) {
139: if (message == SUCCESS_MSG) {
140: // Only queue successful install notifications for retry
141: addInstallNotification(suite.getID(), url);
142: }
143: }
144: }
145:
146: /**
147: * Retry the pending install status message for this suite only.
148: * This method will also retry ALL pending delete notifications,
149: * if the install notification was retried.
150: *
151: * @param token security token of the caller
152: * @param suite MIDlet suite object
153: */
154: public static void retryInstallNotification(SecurityToken token,
155: MIDletSuite suite) {
156: /*
157: * Delay any processing so that startup time is not effected.
158: */
159: new Thread(new InstallRetryHandler(token, suite)).start();
160: }
161:
162: /**
163: * Retry the pending install status message for this suite only.
164: * This method will also retry ALL pending delete notifications,
165: * if the install notification was retried.
166: *
167: * @param token security token of the caller
168: * @param suite MIDlet suite object
169: */
170: static void retryInstallNotificationInternal(SecurityToken token,
171: MIDletSuite suite) {
172: PendingNotification notification = new PendingNotification();
173:
174: token.checkIfPermissionAllowed(Permissions.AMS);
175:
176: // Now, send out install notifications
177: if (suite.getProperty(NOTIFY_PROP) == null) {
178: return;
179: }
180:
181: if (!getInstallNotificationForRetry(suite.getID(), notification)) {
182: return;
183: }
184:
185: try {
186: Protocol httpConnection = new Protocol();
187:
188: httpConnection.openPrim(token, notification.url);
189: postMsgBackToProvider(SUCCESS_MSG, httpConnection, null,
190: null);
191: removeInstallNotification(notification.suiteId);
192: } catch (Throwable t) {
193: if (notification.retries >= Constants.MAX_INSTALL_DELETE_NOTIFICATION_RETRIES) {
194: removeInstallNotification(notification.suiteId);
195: }
196: }
197:
198: try {
199: // Send out delete notifications that have been queued, first
200: postQueuedDeleteMsgsBackToProvider(null, null);
201: } catch (Throwable t) {
202: // ignore
203: }
204: }
205:
206: /**
207: * Posts all queued delete notification messages
208: *
209: * @param proxyUsername if not null, it will be put in the post
210: * @param proxyPassword if not null, it will be put in the post
211: */
212: public static void postQueuedDeleteMsgsBackToProvider(
213: String proxyUsername, String proxyPassword) {
214: PendingNotification[] deleteNotifyList;
215:
216: deleteNotifyList = getDeleteNotifications();
217:
218: for (int i = 0; i < deleteNotifyList.length; i++) {
219: try {
220: postMsgBackToProvider(DELETE_NOTIFICATION_MSG,
221: deleteNotifyList[i].url, proxyUsername,
222: proxyPassword);
223: removeDeleteNotification(deleteNotifyList[i].suiteId);
224: } catch (Throwable t) {
225: if (deleteNotifyList[i].retries >= Constants.MAX_INSTALL_DELETE_NOTIFICATION_RETRIES) {
226: removeDeleteNotification(deleteNotifyList[i].suiteId);
227: }
228: }
229: }
230: }
231:
232: /**
233: * Posts a status message back to the provider's URL in JAD.
234: *
235: * @param message status message to post
236: * @param url target http url for the status message
237: * @param proxyUsername if not null, it will be put in the post
238: * @param proxyPassword if not null, it will be put in the post
239: *
240: * @exception IOException is thrown if any error prevents the
241: * notification from being successful
242: */
243: private static void postMsgBackToProvider(String message,
244: String url, String proxyUsername, String proxyPassword)
245: throws IOException {
246: HttpConnection transaction;
247:
248: if (url == null) {
249: return;
250: }
251:
252: transaction = (HttpConnection) Connector.open(url,
253: Connector.WRITE);
254:
255: postMsgBackToProvider(message, transaction, proxyUsername,
256: proxyPassword);
257: }
258:
259: /**
260: * Posts a status message back to the provider's URL in JAD.
261: *
262: * @param message status message to post
263: * @param transaction http connection to use for posting the status message
264: * @param proxyUsername if not null, it will be put in the post
265: * @param proxyPassword if not null, it will be put in the post
266: *
267: * @exception IOException is thrown if any error prevents the
268: * notification from being successful
269: */
270: private static void postMsgBackToProvider(String message,
271: HttpConnection transaction, String proxyUsername,
272: String proxyPassword) throws IOException {
273: OutputStream out;
274:
275: try {
276: transaction.setRequestMethod(HttpConnection.POST);
277:
278: if (proxyUsername != null && proxyPassword != null) {
279: transaction.setRequestProperty("Proxy-Authorization",
280: formatAuthCredentials(proxyUsername,
281: proxyPassword));
282: }
283:
284: out = transaction.openOutputStream();
285:
286: try {
287: int responseCode;
288:
289: out.write(message.getBytes());
290: responseCode = transaction.getResponseCode();
291: if (responseCode != HttpConnection.HTTP_OK) {
292: throw new IOException("Failed to notify "
293: + transaction.getURL()
294: + " HTTP response code: " + responseCode);
295: }
296: } finally {
297: out.close();
298: }
299: } finally {
300: transaction.close();
301: }
302: }
303:
304: /**
305: * Formats the username and password for HTTP basic authentication
306: * according RFC 2617.
307: *
308: * @param username for HTTP authentication
309: * @param password for HTTP authentication
310: *
311: * @return properly formated basic authentication credential
312: */
313: private static String formatAuthCredentials(String username,
314: String password) {
315: byte[] data = new byte[username.length() + password.length()
316: + 1];
317: int j = 0;
318:
319: for (int i = 0; i < username.length(); i++, j++) {
320: data[j] = (byte) username.charAt(i);
321: }
322:
323: data[j] = (byte) ':';
324: j++;
325:
326: for (int i = 0; i < password.length(); i++, j++) {
327: data[j] = (byte) password.charAt(i);
328: }
329:
330: return "Basic " + Base64.encode(data, 0, data.length);
331: }
332:
333: /**
334: * Retrieves the queued delete notification list. Each element
335: * in the array is a URL to send a delete notification to.
336: *
337: * @return the delete notification list
338: */
339: private static synchronized PendingNotification[] getDeleteNotifications() {
340: PendingNotification[] array = new PendingNotification[getNumberOfDeleteNotifications()];
341:
342: if (array.length > 0) {
343: for (int i = 0; i < array.length; i++) {
344: array[i] = new PendingNotification();
345: }
346:
347: fillDeleteNotificationListForRetry(array);
348: }
349:
350: return array;
351: }
352:
353: /**
354: * Retrieves the number of URLs queued delete in the notification list.
355: *
356: * @return the number of URLs in the delete notification list
357: */
358: private static native int getNumberOfDeleteNotifications();
359:
360: /**
361: * Retrieves the queued delete notification list from storage and
362: * increments the retry count of every member of the list.
363: *
364: * @param list empty delete notification list to fill
365: */
366: private static native void fillDeleteNotificationListForRetry(
367: PendingNotification[] list);
368:
369: /**
370: * Removes the element from the delete notification list.
371: *
372: * @param suiteId suite ID of the notification
373: */
374: private static native void removeDeleteNotification(int suiteId);
375:
376: /**
377: * Adds an element to the install notification list.
378: *
379: * @param suiteId suite the notification belongs to
380: * @param url url to send the notification to
381: */
382: private static native void addInstallNotification(int suiteId,
383: String url);
384:
385: /**
386: * Retrieves the URL of suite's install notification from storage and
387: * increments the retry count of element.
388: *
389: * @param suiteId suite ID of the notification
390: * @param dest where to put the notification
391: *
392: * @return true if the notification is found
393: */
394: private static native boolean getInstallNotificationForRetry(
395: int suiteId, PendingNotification dest);
396:
397: /**
398: * Removes the element from the install notification list.
399: *
400: * @param suiteId suite ID of the notification
401: */
402: private static native void removeInstallNotification(int suiteId);
403: }
404:
405: /** Executes install reties in the background. */
406: final class InstallRetryHandler implements Runnable {
407: /** Security token of the caller. */
408: private SecurityToken token;
409:
410: /** MIDlet suite to retry. */
411: private MIDletSuite suite;
412:
413: /**
414: * Construct a InstallRetryHandler.
415: *
416: * @param theToken security token of the caller
417: * @param theSuite MIDlet suite object
418: */
419: InstallRetryHandler(SecurityToken theToken, MIDletSuite theSuite) {
420: token = theToken;
421: suite = theSuite;
422: }
423:
424: /** Retries after a short delay. */
425: public void run() {
426: try {
427: Thread.sleep(OtaNotifier.RETRY_DELAY);
428: } catch (InterruptedException ie) {
429: // ignore
430: }
431:
432: OtaNotifier.retryInstallNotificationInternal(token, suite);
433: }
434: }
435:
436: /** Pending install or delete notification */
437: final class PendingNotification {
438: /** Number of times the record has been retried. */
439: int retries;
440:
441: /** Suite this notification is for. */
442: int suiteId;
443:
444: /** URL to post the notification to. */
445: String url;
446:
447: /**
448: * Returns a debug string.
449: *
450: * @return value of each field in a string
451: */
452: public String toString() {
453: return "PendingNotification(suite ID = " + suiteId
454: + ", retries = " + retries + ", URL = " + url + ")";
455: }
456: }
|