Tutorial de Java

JDBC y Servlets

Anterior | Siguiente

En esta parte del capítulo dedicado a JDBC, se va a explorar el mundo de los servlets en el contexto de JDBC. Si el lector no está cómodo con el uso de servlets o con los conocimientos que posee, en el Tutorial se trata a fondo este tema, al que puede recurrir y luego regresar a este punto de su lectura.

Hasta ahora se ha presentado la parte cliente del uso de JDBC, y en lo que se pretende adentrar al lector es en el uso de JDBC en el servidor para generar páginas Web a partir de la información obtenida de una base de datos. Si el lector está familiarizado con CGI, podrá comprobar que los servlets son mucho mejores a la hora de generar páginas Web dinámicamente, por varias razones: velocidad, eficiencia en el uso de recursos, escalabilidad, etc.

La arquitectura de los servlets hace que la escritura de aplicaciones que se ejecuten en el servidor sea relativamente sencilla y, eso sí, sean aplicaciones muy robustas. La principal ventaja de utilizar servlets es que se puede programar sin dificultad la información que va a proporcionar entre peticiones del cliente. Es decir, se puede tener constancia de lo que el usuario ha hecho en peticiones anteriores e implementar funciones de tipo rollback o cancel transaction (suponiendo que el servidor de base de datos las soporte). Además, cada instancia del servlet se ejecuta dentro de un hilo de ejecución Java, por lo que se pueden controlar las interacciones entre múltiples instancias; y al utilizar el identificador de sincronización, se puede asegurar que los servlets del mismo tipo esperan a que se produzca la misma transacción, antes de procesar la petición; esto puede ser especialmente útil cuando mucha gente intenta actualizar al mismo tiempo la base de datos, o si hay mucha gente pendiente de consultas a la base de datos cuando ésta está en pleno proceso de actualización.

El ejemplo java2105.java, es un servlet, que junto con la página web asociada, java2105.html, y las dos bases de datos que utiliza, conforma un servicio completo de noticias o artículos. Hay un número determinado de usuarios que tienen autorización para enviar artículos, y otro grupo de usuarios que pueden ver los últimos artículos. Por supuesto, que también se podría almacenar la fecha y la hora en que se colocan los artículos, y posiblemente también fuese útil la categorización, es decir, colocarlos dentro de una categoría como deportes, internacional, nacional, local, etc. De este modo, lo que aquí se esboza como la simple gestión de artículos, podría ser la semilla de un verdadero servicio de noticias. Pero el autor no pretende llegar a eso, sino simplemente presentar la forma en que se puede aunar la fuerza de JDBC y los servlets para poder acceder a una base de datos, introduciendo algunas características como la comprobación de autorización, etc.; por ello, se ha huido conscientemente del uso de la palabra noticias, aunque el lector puede implementar sin demasiada dificultad.

Como el lector es una persona lista, seguro que almacenará toda esta información en una base de datos de la forma mejor posible; de forma que se podría escribir un servlet que acumulase la información que le llegue en ficheros para luego proporcionarla, pero el manejo de todos estos archivos puede resultar engorrosa, y las búsquedas enlentecer el funcionamiento; así que, una base de datos es la mejor de las soluciones.

Además, también se quiere almacenar las contraseñas para el acceso al sistema de artículos en una base de datos, en vez de en la configuración del servidor Web. Hay dos razones fundamentales para ello; por una lado, porque es previsible que mucha gente utilice este servicio, y por otro lado, que debe ser posible asociar cada envío con una persona en particular. La inclusión en una base de datos ayudará a manejar gran cantidad de usuarios y además a controlar quien envía artículos. Una mejora que se puede hacer al sistema es el desarrollo de un applet JDBC que permita añadir y quitar usuarios, o asignar privilegios a quien haya enviado algún artículo al Sistema.

Las siguientes líneas de código muestran las sentencias utilizadas en la creación de las tablas e índices que se van a utilizar en la aplicación que se desarrollará para completar el Sistema de Artículos. El programa se ha pensado para atacar una base de datos Access, utilizando el puente JDBC-ODBC, si el lector desea portarlo a otro driver JDBC, tendría que asegurarse de que las sentencias SQL que se utilizan están soportadas por él.

CREATE TABLE usuarios (
  usuario VARCHAR(16) NOT NULL,
  nombre VARCHAR (60) NOT NULL,
  empresa VARCHAR (60) NOT NULL,
  admitirEnvio CHAR(1) NOT NULL,
  clave VARCHAR (8) NOT NULL );
CREATE INDEX usuario_key ON usuarios(usuario) WITH PRIMARY;

CREATE TABLE articulos (
  titulo VARCHAR(255) NOT NULL,
  usuario VARCHAR(16) NOT NULL,
  cuerpo memo );
CREATE INDEX articulo_key ON articulos(usuario);
CREATE INDEX titulo_key ON articulos(titulo,usuario);

Como se puede observar, solamente hay dos tablas. Si se quisiesen implementar las categorías, sería necesario incorporar una nueva tabla, o añadir un campo más a la tabla de artículos. La clave primaria de la tabla de usuarios es el identificador de cada usuario, y en la tabla de artículos hay un índice compuesto formado por el título del artículo y el usuario que lo envió. Se usa un tipo MEMO (soportado pro Access, que puede ser BLOB en otros motores), para guardar el cuerpo del artículo. Una mejora, en caso de convertirlo en un sistema de noticias, consistiría en añadir la fecha en que se ha enviado la noticia, como ya se ha comentado en un párrafo anterior, guardarla en un campo en la tabla de artículos y añadir el campo al índice compuesto de esa tabla, con lo cual se podrían presentar las noticias correspondientes a una fecha determinada sin aparente dificultad.

En la tabla de usuarios se guarda el identificador por el que el sistema va a reconocer al usuario, junto con la contraseña que elija para comprobar su identificación, más su nombre completo, la empresa a que pertenece y si ese usuario tiene autorización para el envío de artículos al Sistema. Si un usuario no dispone de autorización para el envío, solamente podrá acceder a la lectura de artículos. Y por supuesto, si alguien no aparece en esta tabla, no tendrá acceso alguno al Sistema.

Lo primero que hay que desarrollar es la página que va a dar acceso al sistema, para ver la forma de procesar los datos que van a llegar. La página es muy simple, java2105.html, y a continuación se reproduce la imagen que presenta en el navegador y el código html utilizado para generarla.

<HTML>
<HEAD>
  <TITLE>Tutorial de Java, JDBC</TITLE>
</HEAD>
<BODY>
<FORM ACTION="http://breogan:8080/servlet/java2105" 
  ENCTYPE="x-www-form-encoded" METHOD="POST">
  <H2><CENTER>Tutorial de Java, JDBC</CENTER></H2>
  <P><CENTER><B>Control de Artículos</B></CENTER></P>

  <P>Introduce el nombre por el que te reconoce este Sistema:<BR>
  <INPUT NAME="usuario" TYPE="text" SIZE="16" MAXLENGTH="16"><BR>
  Introduce tu contraseña:<BR>
  <INPUT NAME="clave" TYPE="password" SIZE="8" MAXLENGTH="8"></P>
  Selecciona la acción que quieres realizar:
  <P>
  <INPUT NAME="accion" TYPE="submit" VALUE="Leer Articulos">
  <INPUT NAME="accion" TYPE="submit" VALUE="Envio de Articulos">
  </P>
  </FORM>
<I>Para el envío de artículos se requiere autorización especial</I>
</BODY>
</HTML>

Lo primero que se necesita aclarar es cómo se van a procesar los datos del formulario. La página no puede ser más sencilla, tal como se puede ver en la captura de su visualización en el navegador, con dos botones para seleccionar la acción a realizar. Si se quiere enviar un artículo, no es necesario introducir el nombre y clave en esta página, ya que el servlet enviará una nueva página para la introducción del contenido del artículo, y ya sobre esa sí que se establecen las comprobaciones de si el usuario está autorizado o no a enviar artículos al sistema.

La página que envía el servlet, es la que reproduce la imagen siguiente. Esta página se genera en el mismo momento en que el usuario solicita la inserción de un artículo. En caso de que haya introducido su identificación en la pagina anterior, el servlet la colocará en su lugar, sino, la dejará en blanco.

La imagen reproduce la página ya rellena, lista para la inserción de un nuevo artículo en el sistema. Cuando se pulsa el botón de envío de artículo y el servlet recibe la petición de inserción del artículo en el sistema, es cuando realiza la comprobación de autorización, por una lado de si es un usuario reconocido para el sistema y, en caso afirmativo, si está autorizado al envío de artículo, o solamente puede leerlos.

Debe recordar el lector, que los nombres de los campos de entrada de datos se pueden llamar con getParameter() y recoger la información que contienen. Los parámetros nombre y clave en el formulario se mapean en las variables usuario y clave en el servlet, que serán las que se utilicen para realizar las comprobaciones de acceso del usuario.

El código completo del servlet está en el fichero java2105.java, que se reproduce a continuación.

import java.io.*;
import java.net.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class java2105 extends HttpServlet {
  String DBurl = "jdbc:odbc:Tutorial";
  String usuarioGet = "";
  String usuarioPost = "";
  String claveGet = "";
  String clavePost = "";
  Connection con;
  DatabaseMetaData metaData;

  // Este método es el que se encarga de establecer e inicializar
  // la conexión con la base de datos
  public void init( ServletConfig conf ) throws ServletException {
    SQLWarning w;
                
    super.init( conf );
    try {
      Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
      con = DriverManager.getConnection( DBurl,usuarioPost,clavePost );
      if( (w = con.getWarnings()) != null ) {
        while( w != null ) {
          log( "SQLWarning: "+w.getSQLState()+'\t'+
            w.getMessage()+'\t'+w.getErrorCode()+'\t' );
            w = w.getNextWarning();
          }
        }
    } catch( ClassNotFoundException e ) {
      throw new ServletException( "init" );
    } catch( SQLException e ) {
      try {
        con = DriverManager.getConnection( DBurl,usuarioGet,claveGet );
      } catch( SQLException ee ) {
        ee.printStackTrace();
        while( e != null ) {
          log( "SQLException: "+e.getSQLState()+'\t'+
            e.getMessage()+'\t'+e.getErrorCode() );
          e = e.getNextException();
          }
        throw new ServletException( "init" );
      }
    }
  }

  public void service( HttpServletRequest req,HttpServletResponse res )
    throws ServletException,IOException {
    String usuario = req.getParameter( "usuario" );
    String autorizado;
    String accion = req.getParameter( "accion" );
       
    try {
      autorizado = autorizacion( req );
      if( accion.equals( "Leer Articulos" )
        && !autorizado.equals( "ACCESO DENEGADO" ) ) {
        leerArticulo( req,res );
      } else if( accion.equals( "Enviar" )
        && autorizado.equals( "POST" ) ) {
        enviarArticulo( req,res );
      } else if( accion.equals("Envio de Articulos" ) ) {
        if( usuario == null )
          usuario = " ";
        PrintWriter out = new PrintWriter( res.getOutputStream() );
        out.println( "<HTML>" );
        out.println( "<HEAD><TITLE>Envío de Artículos</TITLE></HEAD>" );
        out.println( "<BODY>" );
        out.println( "<CENTER><H1>Envío de Artículos</H2></CENTER>" );
        out.println( "<FORM  ACTION=http://breogan:8080/servlet/java2105" );
        out.println( "METHOD=POST>" );
        out.println( "<H2>Envío de Artículos</H2>" );
        out.println( "<P>Por este nombre te reconoce el Sistema: <BR>" );
        out.println( "<INPUT NAME=usuario TYPE=text VALUE='"+usuario+"'" );
        out.println( "SIZE=16 MAXLENGTH=16><BR>" );
        out.println( "Introduce tu contraseña: <BR>" );
        out.println( "<INPUT NAME=clave TYPE=password SIZE=8 MAXLENGTH=8>" );
        out.println( "</P>" );
        out.println( "<H3>Título del Artículo a enviar:</H3>" );
        out.println( "<P><INPUT NAME=titulo TYPE=text SIZE=25" );
        out.println( "MAXLENGTH=50></P>" );
        out.println( "<H3>Cuerpo del Artículo</H3>" );
        out.println( "<P><TEXTAREA NAME=cuerpo ROWS=10 COLS=50>" );
        out.println( "</TEXTAREA></P>" );
        out.println( "<P><INPUT NAME=accion TYPE=submit VALUE='Enviar'>" );
        out.println( "</FORM>" );
        out.println( "</BODY></HTML>" );
        out.flush();
      } else {
        PrintWriter out = new PrintWriter( res.getOutputStream() );
        out.println( "<html>" );
        out.println( "<head><title>Acceso Denegado</title></head>" );
        out.println( "<body>" );
        out.println( "Se ha producido un error de acceso:<br>" );
        out.println( "El usuario o clave que has introducido no " );
        out.println( "son válidos o<br>" );
        out.println( "no tienes acceso a esta funcionalidad." );
        out.println( "</body></html>" );
        out.flush();
      }
    } catch( SQLException e ) {
      while( e != null ) {
        log( "SQLException: "+e.getSQLState()+'\t'+
          e.getMessage()+'\t'+e.getErrorCode()+'\t' );
        e = e.getNextException();
        }
    // Aquí habría que insertar el código necesario para restablecer la
    // conexión llamando a init() de nuevo y volviendo a realizar la
    // llamada al método service(req,res)
    }
  }

  // Se cierra la conexión con la base de datos
  public void destroy() {
    try {
      con.close();
    } catch( SQLException e ) {
      while( e != null ) {
        log( "SQLException: "+e.getSQLState()+'\t'+
          e.getMessage()+'\t'+e.getErrorCode()+'\t' );
        e = e.getNextException();
        }
    } catch( Exception e ) {
      e.printStackTrace();
    }
  }

  // Este método ejecuta la consulata a la base de datos y devuelve el
  // resultado, para formatear la salida y presentar el resultado de
  // la consulta de artículos al usuario
  public void leerArticulo( HttpServletRequest req,HttpServletResponse res )
    throws IOException,SQLException {
    Statement stmt = con.createStatement();
    String consulta;
    ResultSet rs;

    res.setStatus( res.SC_OK );
    res.setContentType( "text/html" );
    consulta = "SELECT articulos.cuerpo,articulos.titulo," );
    consulta += articulos.usuario,usuarios.nombre,usuarios.empresa ";
    consulta += "FROM articulos,usuarios WHERE " );
    consulta += articulos.usuario=usuarios.usuario";
    rs = stmt.executeQuery( consulta );

    PrintWriter out = new PrintWriter( res.getOutputStream() );
    out.println( "<HTML>" );
    out.println( "<HEAD><TITLE>Artículos Enviados</TITLE></HEAD>" );
    out.println( "<BODY>" );

    while( rs.next() ) {
      out.println( "<H2>" );
      out.println( rs.getString(1) );
      out.println( "</H2><p>" );
      out.println( "<I>Enviado desde: "+rs.getString(5)+"</I><BR> " );
      out.println( "<B>"+rs.getString(2)+"</B>, por "+rs.getString(4) );
      out.println( "<HR>" );
      }
    out.println( "</BODY></HTML>" );
    out.flush();
    rs.close();
    stmt.close();
    }

  public void enviarArticulo( HttpServletRequest req,HttpServletResponse res )
    throws IOException,SQLException {
    Statement stmt = con.createStatement();
    String consulta = "";
    String usuario = req.getParameter( "usuario" );

    PrintWriter out = new PrintWriter( res.getOutputStream() );
    res.setStatus( res.SC_OK );
    res.setContentType( "text/html" );
    out.println( "<HTML>" );
    out.println( "<HEAD><TITLE>Envío Realizado</TITLE></HEAD>" );
    out.println( "<BODY>" );

    consulta = "INSERT INTO articulos VALUES( '";
    consulta += req.getParameter( "titulo" )+"','"+usuario+"','";
    consulta += req.getParameter("cuerpo")+"')";
    int result = stmt.executeUpdate( consulta );

    if( result != 0 ) {
      out.println( "Tu artículo ha sido aceptado e insertado" );
      out.println( " correctamente." );
    } else { 
      out.println( "Se ha producido un error en la aceptación de tu " );
       out.println( "artículo.<BR>" );
      out.println( "Contacta con el Administrador de la base de datos, " );
       out.println( "o consulta<BR>" );
      out.println( "el fichero <I>log</I> del servlet." );
    }
    out.println( "</BODY></HTML>" );
    out.flush();
    stmt.close();
  }

  // Devuelve la información del Servlet
  public String getServletInfo() {
    return "Servlet JDBC (Tutorial de Java), 1998";
  }

  public String autorizacion( HttpServletRequest req ) throws SQLException {
    Statement stmt = con.createStatement();
    String consulta;
    ResultSet rs;
    String valido = "";
    String usuario = req.getParameter( "usuario" );
    String clave = req.getParameter( "clave" );
    String permiso="";

    consulta = "SELECT admitirEnvio FROM usuarios WHERE usuario = '"+usuario;
    consulta += "' AND clave = '"+clave+"'";
    rs = stmt.executeQuery( consulta );

    while( rs.next() ) {
      valido = rs.getString(1);
    } 
    rs.close();
    stmt.close();

    if( valido.equals( "" ) ) {
      permiso = "ACCESO DENEGADO";
    } else {
      // Permiso sólo para lectura de artículos
      if( valido.equals( "N" ) ) {
        permiso = "GET";
      // Permiso para lectura y envío de artículos
      } else if( valido.equals( "S" ) ) {
        permiso = "POST";
      }
    }
  return permiso;
  }
}

A continuación se repasan los trozos de código más interesantes del servlet, tal como se ha hecho en muchos de los ejemplos del Tutorial. Una de las primeras cosas que hay que hace es reconocer cuando se ha recibido una petición correctamente, y para ello se utiliza el código que aparece en la línea siguiente:

res.setStatus( res.SC_OK );

La línea que sigue a ésta, es la que indica que el contenido es HTML, porque la intención del servlet es enviar respuestas en este formato al navegador. Esto se indica en la línea:

res.setContentType( "text/html" );

Otro trozo de código interesante es el utilizado para saber cual de los botones de la página inicial se ha pulsado, en donde se recurre al método getParameter() sobre el nombre del botón (accion), buscando los valores que se han asignado en el código fuente, y procesar el que se haya pulsado de los dos. Las líneas de código siguientes son las que realizan estas acciones.

String accion = req.getParameter( "accion" );
try {
  autorizado = autorizacion( req );
  if( accion.equals( "Leer Articulos" )
    && !autorizado.equals( "ACCESO DENEGADO" ) ) {
    leerArticulo( req,res );
  } else if( accion.equals( "Enviar" )
    && autorizado.equals( "POST" ) ) {
    enviarArticulo( req,res );
  } else if( accion.equals("Envio de Articulos" ) ) {
  . . .
  }
} catch( SQLException e ) {
  . . .
  }

Como se ha especificado en la página inicial de acceso un valor para cada uno de los botones, se sabe cuales deben buscarse y qué hacer para procesarlos. También se pueden recoger todos los parámetros que hay en un formulario a través del método getParameterNames().

Una vez que se sabe el usuario y la clave, hay que comprobar esta información contra la base de datos. Para ello se utiliza una consulta para saber si el usuario figura en la base de datos. El código que realiza estas acciones es el que se reproduce en las siguientes líneas.

public String autorizacion( HttpServletRequest req ) throws SQLException {
    Statement stmt = con.createStatement();
    String consulta;
    ResultSet rs;
    String valido = "";
    String usuario = req.getParameter( "usuario" );
    String clave = req.getParameter( "clave" );
    String permiso="";

    consulta = "SELECT admitirEnvio FROM usuarios WHERE usuario = '"+usuario;
    consulta += "' AND clave = '"+clave+"'";
    rs = stmt.executeQuery( consulta );

    while( rs.next() ) {
      valido = rs.getString(1);
    } 
    rs.close();
    stmt.close();

    if( valido.equals( "" ) ) {
      permiso = "ACCESO DENEGADO";
    } else {
      // Permiso sólo para lectura de artículos
      if( valido.equals( "N" ) ) {
        permiso = "GET";
      // Permiso para lectura y envío de artículos
      } else if( valido.equals( "S" ) ) {
        permiso = "POST";
      }
    }
  return permiso;
  }

Para realizar las consultas a la base de datos, es necesario crear una conexión con ella. Para hacerlo se utiliza el método init() de la clase HttpServlet, en donde se instancia la conexión con el servidor de base de datos, como se muestra en las líneas de código siguientes.

String DBurl = "jdbc:odbc:Tutorial";
. . .
try {
  Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
  con = DriverManager.getConnection( DBurl,usuarioPost,clavePost );
. . .

Esta es solamente la parte de la conexión, hay más código implicado, pero ya se han visto bastantes ejemplos. No obstante, hay que tener en cuenta que la conexión se puede romper normalmente por tiempo, es decir, que si no se realizan acciones contra la base de datos en un tiempo determinado, la conexión se rompe. Así que, una de las cosas que debe controlar el lector, si va a utilizar este código es introducir la reconexión en la parte de código que trata la excepción de SQL..

Una vez creada la conexión, hay que realizar consultas para la lectura de datos. El siguiente código hace esto, consultando la base de datos y recogiendo la información de todos los artículos. Se puede incorporar fácilmente un nuevo campo con la fecha, para obtener los artículos ordenados por la fecha en que ha sido enviados, o realizar consultas diferentes para obtener los artículos ordenados por usuario, etc.

    consulta = "SELECT articulos.cuerpo,articulos.titulo," );
    consulta += articulos.usuario,usuarios.nombre,usuarios.empresa ";
    consulta += "FROM articulos,usuarios WHERE " );
    consulta += articulos.usuario=usuarios.usuario";
    rs = stmt.executeQuery( consulta );

    PrintWriter out = new PrintWriter( res.getOutputStream() );
    out.println( "<HTML>" );
    out.println( "<HEAD><TITLE>Artículos Enviados</TITLE></HEAD>" );
    out.println( "<BODY>" );

    while( rs.next() ) {
      out.println( "<H2>" );
      out.println( rs.getString(1) );
      out.println( "</H2><p>" );
      out.println( "<I>Enviado desde: "+rs.getString(5)+"</I><BR> " );
      out.println( "<B>"+rs.getString(2)+"</B>, por "+rs.getString(4) );
      out.println( "<HR>" );
      }
    out.println( "</BODY></HTML>" );
    out.flush();
    rs.close();
    stmt.close();
    }

Aquí se llama al método getString() de cada columna de cada fila, ya que cada fila corresponde a un artículo. La figura siguiente muestra el resultado de la ejecución de una consulta de este tipo.

Otra parte interesante del código es la que se encarga del envío de los artículos y su inserción en la base de datos. Esto se realiza en las líneas que se muestran. Las cuestiones de autorización se encargan a otro método, así que en este no hay porqué considerarlas.

    consulta = "INSERT INTO articulos VALUES( '";
    consulta += req.getParameter( "titulo" )+"','"+usuario+"','";
    consulta += req.getParameter("cuerpo")+"')";
    int result = stmt.executeUpdate( consulta );

    if( result != 0 ) {
      out.println( "Tu artículo ha sido aceptado e insertado" );
      out.println( " correctamente." );
    } else { 
      out.println( "Se ha producido un error en la aceptación de tu " );
       out.println( "artículo.<BR>" );
      out.println( "Contacta con el Administrador de la base de datos, " );
       out.println( "o consulta<BR>" );
      out.println( "el fichero <I>log</I> del servlet." );
    }
    out.println( "</BODY></HTML>" );
    out.flush();
    stmt.close();
  }

Con esto, se ha presentado al lector un ejemplo en el que se accede a la base de datos desde el servidor, y la parte cliente se limita a utilizar los recursos del servidor Web para acceder a la información de la base de datos. El ejemplo es específico para servlets HTTP, aunque se pueden escribir servlets que devuelvan tipos binarios en lugar de una página html; Por ejemplo, se puede fijar el tipo de contenido a ‘image/gif’ y utilizar el controlador de OutputStream para escribir una imagen gif que se construya en el mismo momento al navegador. De este modo se pueden pasar imágenes que están almacenadas en la base de datos del servidor a la parte cliente, en este caso, el navegador.

Navegador

Home | Anterior | Siguiente | Indice | Correo