miércoles, 12 de marzo de 2008

Leer y escribir archivos en J2ME

En muchos casos se necesita leer y escribir archivos a las unidades de almacenamiento del teléfono. Uno de los escenarios posibles es la exportación e importación de datos.

En esta nota voy a mostrar breves ejemplos de como realizar estas tareas utilizando la API FileConnection.

La clase FileConnection forma parte de los paquetes opcionales a J2ME. Por ese motivo al crear un nuevo proyecto en Sun Wireless Toolkit, al momento de elegir la API, debemos marcar:
Target Platform = JTWI y tildar PDA Profile for J2ME (JSR 75)


Hay que destacar que al ser una especificación optativa puede no estar presente en todos los teléfonos o dispositivos. En general todos los teléfonos actuales lo soportan, pero no es así para los modelos viejos.

En el emulador el sistema de archivos "virtual" se encuentra en la carpeta "..j2mewtk\2.5.2\appdb\DefaultColorPhone\filesystem\".

Dentro de esta carpeta podemos encontrar otra llamada root1. Cada carpeta en "filesystem" es tomada como una unidad o root por el emulador. En el télefono cada root corresponderá a un dispositivo de almacenamiento diferente, en la mayoría de los casos uno será la memoria interna del teléfono y otro la tarjeta de memoria.

El primer ejemplo trata de como listar todos los roots disponibles. Esto se logra utilizando la clase FileSystemRegistry:

public void SelectRoot() {
Enumeration rootListEnum = FileSystemRegistry.listRoots();
listRoots = new List("Elija el root:", Choice.EXCLUSIVE);
while (rootListEnum.hasMoreElements()) {
String rootName = (String) rootListEnum.nextElement();
listRoots.append(rootName, null);
}
...


Este fragmento utiliza la función listRoots() que devuelve una lista de los roots y luego la carga en un control de lista para mostrarla en la pantalla.

Para listar el contenido de un directorio:

Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();



Las tres primeras líneas son obligatorias cuando estemos utilizando un CommandListener por lo expuesto en Acceso a archivos y threads, su finalidad es evitar que la aplicación se cuelgue si hay un problema en las operaciones de manejo de archivos.

En lo demás este caso es similar al de listado de roots, utilizando el método "list".

Finalmente para escribir un archivo debemos utlizar además un Data Stream:


Thread t = new Thread() {
public void run() {
try{

FileConnection conn = (FileConnection) Connector.open(writeFile, Connector.READ_WRITE);
if (!conn.exists()) {
conn.create();
}
OutputStream outSt = conn.openOutputStream();
PrintStream printSt = new PrintStream(outSt);
printSt.println(writeContents);
conn.close();

} catch(IOException e) {
System.out.println("IOException in WriteToFile: " + e.toString());
} catch(SecurityException e) {
System.out.println("SecurityException in WriteToFile: " + e.toString());
}
}
};
t.start();


La referencia de este paquete opcional se encuentra en:
JSR-000075 PDA Optional Packages for the J2METM Platform. Ahí puede descargarse las especificaciones de FileConnection con todas sus clases y métodos.
Y una serie de artículos sobre esto en:
Mobile Service Architecture - JSR 75



El código completo de esta clase ejemplo es:
import javax.microedition.midlet.MIDlet;
import javax.microedition.io.file.*;
import java.io.*;
import java.util.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;

public class FileDemo extends MIDlet implements CommandListener {
List listRoots, listContents;
private String currentRoot, currentPath, writeFile, writeContents;
private Command cmOK,cmExit, cmNew;
private Form WaitForm;

public void FileDemo() {
}

public void startApp() {
WaitForm = new Form("Espere por favor...");
WaitForm.setCommandListener(this);
cmExit = new Command("Exit", Command.EXIT, 1);
cmNew = new Command("New", Command.ITEM,2);
SelectRoot();
}

public void pauseApp() {
}

public void destroyApp(boolean condition) {
notifyDestroyed();
}

public void SelectRoot() {
Enumeration rootListEnum = FileSystemRegistry.listRoots();
listRoots = new List("Elija el root:", Choice.EXCLUSIVE);
while (rootListEnum.hasMoreElements()) {
String rootName = (String) rootListEnum.nextElement();
listRoots.append(rootName, null);
}
cmOK = new Command("OK", Command.ITEM, 1);

listRoots.addCommand(cmOK);
listRoots.addCommand(cmExit);
listRoots.setCommandListener(this);
Display.getDisplay(this).setCurrent(listRoots);


}

public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);
Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();
listContents.addCommand(cmExit);
listContents.addCommand(cmNew);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);

}

public void WriteToFile(String fileName, String contents) {
writeFile = fileName;
writeContents = contents;

Thread t = new Thread() {
public void run() {
try{

FileConnection conn = (FileConnection) Connector.open(writeFile, Connector.READ_WRITE);
if (!conn.exists()) {
conn.create();
}
OutputStream outSt = conn.openOutputStream();
PrintStream printSt = new PrintStream(outSt);
printSt.println(writeContents);
conn.close();

} catch(IOException e) {
System.out.println("IOException in WriteToFile: " + e.toString());
} catch(SecurityException e) {
System.out.println("SecurityException in WriteToFile: " + e.toString());
}
}
};
t.start();
}

public void commandAction(Command cm, Displayable d) {
//System.out.println("En commandAction");
if (cm == cmOK && d == listRoots) {
System.out.println("Elegiste: " + listRoots.getString(listRoots.getSelectedIndex()));
currentRoot = listRoots.getString(listRoots.getSelectedIndex());
ShowContents();
} else if (cm == cmExit) {
destroyApp(false);
} else if (cm == cmNew) {
WriteToFile("file:///" + currentRoot + "newfile.txt","Sample, sample, sample");
}
}
}

martes, 11 de marzo de 2008

Acceso a archivos y threads

Corriendo un ejemplo de escritura de archivos en Sun Java Wireless Toolkit sucedía que la aplicación se colgaba al momento de abrir el archivo.

En la consola se mostraba este mensaje:
Warning: To avoid potential deadlock, operations that may block, such as
networking, should be performed in a different thread than the
commandAction() handler.


El mismo problema aparecía en el ejemplo de lectura de un directorio que estaba escribiendo. En ambos casos la aplicación se colgaba al llamar a Connector.open.

La advertencia en cuestión quedaría traducida a:
"Para evitar posibles puntos muertos, las operaciones que pueden bloquear, como conexiones de red, deben ser ejecutadas en un hilo (thread) diferente que el administrador de commandAction()"

El "commandAction() handler" sería la clase que especificamos para manejar la interacción con el usuario mediante el método setCommandListener.

Como un conexión de red o un acceso a un archivo puede dejar inutilizable la interfaz de usuario, debe correrse en un thread separado.

Una muy buena explicación de esto puede encontrarse en:
Networking, User Experience, and Threads

En cuanto a la solución de este error en particular fue cambiar este código:


public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);

try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}

listContents.addCommand(cmExit);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);
}

Por este:

public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);
Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();
listContents.addCommand(cmExit);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);

}

De acuerdo a lo explicado en el artículo citado.

Para ver un ejemplo de como listar roots y escribir archivos ir a:
Leer y escribir archivos en J2ME

Uso de la clase java.util.Stack en flujo de aplicaciones J2ME

Una pila (stack en inglés) es una estructura de datos de tipo LIFO (del inglés Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos. Se aplica en multitud de ocasiones en informática debido a su simplicidad y ordenación implícita en la propia estructura.

Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, retirar (o desapilar, pop), que retira el último elemento apilado. Ver Pila (estructura de datos) en Wikipedia.

Java provee una implementación de esta estructura de datos en la clase java.util.Stack que cuenta con los métodos empty, peek, pop, push y search.

Push: agrega un elemento a la pila.
Pop: recupera el último elemento y lo elimina.
Peek: recupera el último elemento sin eliminarlo de la pila.

Un ejemplo de utilización de esta clase puede ser para almacenar el flujo de una aplicación J2ME como se ve en este ejemplo adaptado del código de Cash2ME:


package net.sf.cash2me;

import java.util.Stack;

import javax.microedition.lcdui.Displayable;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

public class Cash2ME extends MIDlet {

private Displayable currentScreen; // La pantalla actual
private Stack screenStack; // contiene todas las pantallas previas

/**
* Recibe un objeto Displayable y lo configura como Current de Display, es decir
* lo muestra en la pantalla.
* @param screen El objeto Displayable a mostrar.
*/
private void setCurrent(Displayable screen) {
if (currentScreen == null || screen != currentScreen) {
if (screenStack == null)
screenStack = new Stack();
screenStack.push(currentScreen); //almacena la pantalla actual en la pila.
currentScreen = screen;
}
Display.getDisplay(this).setCurrent(currentScreen); //muestra la pantalla recibida
}

/**
* Muestra la pantalla anterior.
*/
public void showPrevious() {
if (screenStack != null) {
/*Controla si hay más de una pantalla en la pila*/
Displayable screen = screenStack.size() > 1
/*Si hay más de una pantalla en la pila recupera la última y la elimina de la pila.*/
? (Displayable)screenStack.pop()
/* Si queda solo una, utiliza el método peek para recuperar la última pantalla
sin borrarla de la pila.*/

: (Displayable)screenStack.peek();
if (screen != null)
Display.getDisplay(this).setCurrent(currentScreen = screen);
}
}
}


En este ejemplo en lugar de recurrir directamente al método Display.setCurrent se recurre a estos métodos para llevar un "historial" de las pantallas recorridas. De esta forma cada vez que a lo largo de la aplicación se muestra una pantalla nueva, primero se guarda la pantalla actual en la pila y luego se muestra la nueva.

Cuando la aplicación necesita volver a la pantalla anterior, recupera la última pantalla, que estará arriba de la pila, la muestra y la elimina, usando el método pop.

Si se llega al final de la pila, quedando una sola pantalla en la pila, se utiliza el método peek para recuperar dicha pantalla sin eliminarla.

Más Información
Otro ejemplo de pilas en java: How do I use stacks in Java?
Referencia de la clase Stack: Class Stack