/** * HTTPDemo.java * * Copyright � 1998-2011 Research In Motion Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Note: For the sake of simplicity, this sample application may not leverage * resource bundles and resource strings. However, it is STRONGLY recommended * that application developers make use of the localization features available * within the BlackBerry development platform to ensure a seamless application * experience across a variety of languages and geographies. For more information * on localizing your application, please refer to the BlackBerry Java Development * Environment Development Guide associated with this release. */ package com.rim.samples.device.httpdemo; import java.io.IOException; import java.io.InputStream; import javax.microedition.io.Connector; import javax.microedition.io.HttpConnection; import net.rim.device.api.command.Command; import net.rim.device.api.command.CommandHandler; import net.rim.device.api.command.ReadOnlyCommandMetadata; import net.rim.device.api.io.IOCancelledException; import net.rim.device.api.io.IOUtilities; import net.rim.device.api.system.Characters; import net.rim.device.api.ui.MenuItem; import net.rim.device.api.ui.UiApplication; import net.rim.device.api.ui.component.BasicEditField; import net.rim.device.api.ui.component.Dialog; import net.rim.device.api.ui.component.EditField; import net.rim.device.api.ui.component.Menu; import net.rim.device.api.ui.component.RichTextField; import net.rim.device.api.ui.component.SeparatorField; import net.rim.device.api.ui.container.MainScreen; import net.rim.device.api.util.StringProvider; /** * This sample makes a an http or https connection to a specified URL and * retrieves and displays html content. */ public class HTTPDemo extends UiApplication { private static final String SAMPLE_HTTPS_PAGE = "https://www.blackberry.com/go/mobile/samplehttps.shtml"; private static final String[] HTTP_PROTOCOL = { "http://", "http:\\", "https://", "https:\\" }; private static final char HTML_TAG_OPEN = '<'; private static final char HTML_TAG_CLOSE = '>'; private static final String HEADER_CONTENTTYPE = "content-type"; private static final String CONTENTTYPE_TEXTHTML = "text/html"; private static final int STATE_0 = 0; private static final int STATE_1 = 1; private static final int STATE_2 = 2; private static final int STATE_3 = 3; private static final int STATE_4 = 4; private static final int STATE_5 = 5; private static final char CR = 0x000D; private static final char LF = 0x000A; private static final char TAB = 0x0009; private final HTTPDemoScreen _mainScreen; private final EditField _url; private final RichTextField _content; private boolean _useWapStack; private final WapOptionsScreen _wapOptionsScreen; private StatusThread _statusThread = new StatusThread(); private ConnectionThread _connectionThread = new ConnectionThread(); /** * Entry point for application * * @param args * Command line arguments (not used) */ public static void main(final String[] args) { // Create a new instance of the application and make the currently // running thread the application's event dispatch thread. final HTTPDemo theApp = new HTTPDemo(); theApp.enterEventDispatcher(); } /** * Creates a new HTTPDemo object */ public HTTPDemo() { _fetchMenuItem = new MenuItem(new StringProvider("Fetch"), 0x230010, 0); _fetchMenuItem.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { // Don't execute on a blank url. if (_url.getText().length() > 0) { if (!_connectionThread.isStarted()) { fetchPage(_url.getText()); } else { createNewFetch(_url.getText()); } } } })); _clearContent = new MenuItem(new StringProvider("Clear Content"), 0x230020, 3); _clearContent.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { _content.setText("<content>"); } })); _fetchHTTPSPage = new MenuItem(new StringProvider("Fetch Sample HTTPS Page"), 0x230030, 2); _fetchHTTPSPage.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { if (!_connectionThread.isStarted()) { // Menu items are executed on the event thread, therefore we // can edit the // URL field in place. _url.setText(SAMPLE_HTTPS_PAGE); fetchPage(SAMPLE_HTTPS_PAGE); } else { createNewFetch(_url.getText()); } } })); _wapStackOption = new MenuItem(new StringProvider("Use Wap Stack"), 0x230040, 4); _wapStackOption.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { _useWapStack = !_useWapStack; // Toggle the wap stack option } })); _wapStackOptionScreen = new MenuItem(new StringProvider("Wap Options"), 0x230050, 5); _wapStackOptionScreen.setCommand(new Command(new CommandHandler() { /** * @see net.rim.device.api.command.CommandHandler#execute(ReadOnlyCommandMetadata, * Object) */ public void execute(final ReadOnlyCommandMetadata metadata, final Object context) { _wapOptionsScreen.display(); } })); _wapOptionsScreen = new WapOptionsScreen(this); _mainScreen = new HTTPDemoScreen(); _mainScreen.setTitle("HTTP Demo"); _url = new EditField("URL: ", "http://", Integer.MAX_VALUE, BasicEditField.FILTER_URL); _url.setCursorPosition(7); _mainScreen.add(_url); _mainScreen.add(new SeparatorField()); _content = new RichTextField(); _mainScreen.add(_content); // Start the helper threads _statusThread.start(); _connectionThread.start(); pushScreen(_mainScreen); } /** * Menu item to fetch content from URL specified in URL field */ private final MenuItem _fetchMenuItem; /** * Clears the content field */ private final MenuItem _clearContent; /** * Menu item to fetch pre-defined sample HTTPS page */ private final MenuItem _fetchHTTPSPage; /** * Toggles the wap stack option */ private final MenuItem _wapStackOption; /** * Menu item to display the wap options screen */ private final MenuItem _wapStackOptionScreen; /** * Stops current fetch and initiates a new fetch * * @param url * The url of the content to fetch */ private void createNewFetch(final String url) { // Stop the current helper threads _statusThread.stop(); _connectionThread.stop(); // Reinitialize the helper threads _statusThread = new StatusThread(); _connectionThread = new ConnectionThread(); // Restart the helper threads _statusThread.start(); _connectionThread.start(); // Fetch the url fetchPage(url); } /** * Fetches the content on the speicifed url * * @param url * The url of the content to fetch */ private void fetchPage(String url) { // Normalize the url final String lcase = url.toLowerCase(); boolean validHeader = false; int i = 0; for (i = HTTP_PROTOCOL.length - 1; i >= 0; --i) { if (-1 != lcase.indexOf(HTTP_PROTOCOL[i])) { validHeader = true; break; } } if (_useWapStack) { url = url + _wapOptionsScreen.getWapParameters(); } if (!validHeader) { url = HTTP_PROTOCOL[0] + url; // Prepend the protocol specifier } // It is illegal to open a connection on the event thread. We need to // spawn a new thread for connection operations. _connectionThread.fetch(url); // Create a thread to display the status of the current operation _statusThread.go(); } /** * Method to update the content field * * @param text * The text to display */ private void updateContent(final String text) { // This will create significant garbage, but avoids threading issues // (compared with creating a static Runnable and setting the text). UiApplication.getUiApplication().invokeLater(new Runnable() { public void run() { _content.setText(text); } }); } /** * Performs operations on the html data. Removes tags, comments, whitespace * and inserts new lines for the * <p> * tag. * * @param text * The text to be prepared for display * @return The processed text */ private String prepareData(final String text) { final int text_length = text.length(); final StringBuffer data = new StringBuffer(text_length); int state = STATE_0; int count = 0; int writeIndex = -1; char c = (char) 0; for (int i = 0; i < text_length; ++i) { c = text.charAt(i); switch (state) { case STATE_0: if (c == HTML_TAG_OPEN) { ++count; state = STATE_1; } else if (c == ' ') { data.insert(++writeIndex, c); state = STATE_5; } else if (!specialChar(c)) { data.insert(++writeIndex, c); } break; case STATE_1: if (c == '!' && text.charAt(i + 1) == '-' && text.charAt(i + 2) == '-') { System.out.println("Entering Comment state"); i += 2; state = STATE_3; } else if (Character.toLowerCase(c) == 'p') { state = STATE_4; } else if (c == HTML_TAG_CLOSE) { --count; state = STATE_0; } else { state = STATE_2; } break; case STATE_2: if (c == HTML_TAG_OPEN) { ++count; } else if (c == HTML_TAG_CLOSE) { if (--count == 0) { state = STATE_0; } } break; case STATE_3: if (c == '-' && text.charAt(i + 1) == '-' && text.charAt(i + 2) == HTML_TAG_CLOSE) { --count; i += 2; state = STATE_0; System.out.println("Exiting comment state"); } break; case STATE_4: if (c == HTML_TAG_CLOSE) { --count; data.insert(++writeIndex, '\n'); state = STATE_0; } else { state = STATE_1; } break; case STATE_5: if (c == HTML_TAG_OPEN) { ++count; state = STATE_1; } else if (c != ' ') { state = STATE_0; if (!specialChar(c)) { data.insert(++writeIndex, c); } } break; } } return data.toString().substring(0, writeIndex + 1); } /** * Checks whether a char is a carriage return, line feed or tab character * * @param c * The char to check * @return True if char is a carriage return or a line feed, otherwise false */ private boolean specialChar(final char c) { return c == LF || c == CR || c == TAB; } /** * The ConnectionThread class manages the HTTP connection. If a fetch call * is made and another request is made while the first is still active, the * first fetch will be terminated and the second one will start processing. */ private class ConnectionThread extends Thread { private static final int TIMEOUT = 500; // ms private String _theUrl; private volatile boolean _fetchStarted = false; private volatile boolean _stop = false; /** * Retrieves the url this thread is trying to connect to * * @return The url that this thread is trying to connect to */ private String getUrl() { return _theUrl; } /** * Indicates whether the thread has started fetching yet * * @return True if the fetching has started, false otherwise */ private boolean isStarted() { return _fetchStarted; } /** * Fetches a page * * @param url * The url of the page to fetch */ private void fetch(final String url) { _fetchStarted = true; _theUrl = url; } /** * Stop the thread */ private void stop() { _stop = true; } /** * This method is where the thread retrieves the content from the page * whose url is associated with this thread. * * @see java.lang.Runnable#run() */ public void run() { for (;;) { // Thread control while (!_fetchStarted && !_stop) { // Sleep for a bit so we don't spin try { sleep(TIMEOUT); } catch (final InterruptedException e) { errorDialog("Thread#sleep(long) threw " + e.toString()); } } // Exit condition if (_stop) { return; } String content = ""; // Open the connection and extract the data try { final HttpConnection httpConn = (HttpConnection) Connector.open(getUrl()); final int status = httpConn.getResponseCode(); if (status == HttpConnection.HTTP_OK) { // Is this html? final String contentType = httpConn.getHeaderField(HEADER_CONTENTTYPE); final boolean htmlContent = contentType != null && contentType .startsWith(CONTENTTYPE_TEXTHTML); final InputStream input = httpConn.openInputStream(); final byte[] bytes = IOUtilities.streamToBytes(input); final StringBuffer raw = new StringBuffer(new String(bytes)); raw.insert(0, "bytes received]\n"); raw.insert(0, bytes.length); raw.insert(0, '['); content = raw.toString(); if (htmlContent) { content = prepareData(raw.toString()); } input.close(); } else { content = "response code = " + status; } httpConn.close(); } catch (final IOCancelledException e) { System.out.println(e.toString()); return; } catch (final IOException e) { errorDialog(e.toString()); return; } // Make sure status thread doesn't overwrite the content stopStatusThread(); updateContent(content); // We're finished with the operation so reset // the start state. _fetchStarted = false; } } /** * Stops the status thread */ private void stopStatusThread() { _statusThread.pause(); try { synchronized (_statusThread) { // Check the paused condition, in case the notify // fires prior to our wait, in which case we may // never see that notify. while (!_statusThread.isPaused()) { ; } { _statusThread.wait(); } } } catch (final InterruptedException e) { errorDialog("StatusThread#wait() threw " + e.toString()); } } } /** * The StatusThread class manages display of the status message while * lengthy HTTP/HTML operations are taking place. */ private class StatusThread extends Thread { private static final int TIMEOUT = 500; // ms private static final int THREAD_TIMEOUT = 500; private volatile boolean _stop = false; private volatile boolean _running = false; private volatile boolean _isPaused = false; /** * Resumes this thread * * @see #pause() */ private void go() { _running = true; _isPaused = false; } /** * Pauses this thread * * @see #go() */ private void pause() { _running = false; } /** * Queries the paused status of this thread * * @return True if the thread is paused, false otherwise */ private boolean isPaused() { return _isPaused; } /** * Stops the thread */ private void stop() { _stop = true; } /** * This method is where the thread updates the status message while * HTTP/HTML operations are taking place. * * @see java.lang.Runnable#run() */ public void run() { int i = 0; // Set up the status messages final String[] statusMsg = new String[6]; final StringBuffer status = new StringBuffer("Working"); statusMsg[0] = status.toString(); for (int j = 1; j < 6; ++j) { statusMsg[j] = status.append(" .").toString(); } for (;;) { while (!_stop && !_running) { // Sleep a bit so we don't spin try { sleep(THREAD_TIMEOUT); } catch (final InterruptedException e) { errorDialog("Thread#sleep(long) threw " + e.toString()); } } if (_stop) { return; } i = 0; // Clear the status buffer status.delete(0, status.length()); for (;;) { // We're not synchronizing on the boolean flag, // therefore, value is declared volatile. if (_stop) { return; } if (!_running) { _isPaused = true; synchronized (this) { this.notify(); } break; } updateContent(statusMsg[++i % 6]); try { Thread.sleep(TIMEOUT); // Wait for a bit. } catch (final InterruptedException e) { errorDialog("Thread.sleep(long) threw " + e.toString()); } } } } } /** * This is the main screen that displays the content fetched by the * ConnectionThread. */ private class HTTPDemoScreen extends MainScreen { /** * @see net.rim.device.api.ui.container.MainScreen#makeMenu(Menu,int) */ protected void makeMenu(final Menu menu, final int instance) { menu.add(_fetchMenuItem); menu.add(_clearContent); menu.add(_fetchHTTPSPage); menu.add(_wapStackOptionScreen); final StringBuffer sb = new StringBuffer(); if (_useWapStack) { sb.append(Characters.CHECK_MARK); } sb.append("Use Wap Stack"); _wapStackOption.setText(new StringProvider(sb.toString())); menu.add(_wapStackOption); menu.addSeparator(); super.makeMenu(menu, instance); } /** * @see net.rim.device.api.ui.container.MainScreen#onSavePrompt() */ public boolean onSavePrompt() { // Prevent the save dialog from being displayed return true; } /** * @see net.rim.device.api.ui.Screen#close() */ public void close() { _statusThread.stop(); _connectionThread.stop(); super.close(); } /** * @see net.rim.device.api.ui.Screen#keyChar(char,int,int) */ protected boolean keyChar(final char key, final int status, final int time) { if (getLeafFieldWithFocus() == _url && key == Characters.ENTER) { _fetchMenuItem.run(); return true; // Consume the key event } else { return super.keyChar(key, status, time); } } } /** * Presents a dialog to the user with a given message * * @param message * The text to display */ public static void errorDialog(final String message) { UiApplication.getUiApplication().invokeLater(new Runnable() { public void run() { Dialog.alert(message); } }); } }