Reporte II: MicroOJ & servicios Web

2016-02-18 2024-05-07 devpost

Este es el reporte correspondiente al proyecto número 2 de la materia Desarrollo de Aplicaciones para Tecnologías Móviles.

Definición del Problema

Este proyecto consiste en desarrollar un Web Service que manipule una base de datos a petición de los clientes.

Desarrolle un Web Service con los servicios necesarios para manipular una base de datos (backend) y además una aplicación cliente para que los usuarios puedan acceder a las servicios (frontend).

Definiciones

Antes de continuar es necesario conocer algunas definiciones para comprender mejor el trabajo que vamos a realizar.

Web Service

Un Web service es un sistema de software diseñado para soportar interacción máquina-a-máquina de manera interoperable a través de una red. Éste sistema tiene una interfaz descrita en un formato que puede ser procesado por la máquina (específicamente WSDL). Otros sistemas interactúan con el Web service en una manera prescrita por su definición usando mensajes SOAP, comúnmente transmitidos usando HTTP con una serialización XML en conjunto con otros estándares Web.

Propuesta de solución

El problema nos solicita dos cosas, proveer servicios y manipular una base de datos. En vista que el problema no especifica que servicios ni que datos manipular, tenemos la libertad de enfocar el proyecto a la problemática que más nos agrade.

Vamos a desarrollar un mina sistema de juez en línea como UVa, LightOJ u OmegaUp, el cual consiste en dos partes, un servidor (el juez) y uno o más clientes (los solucionadores de problemas).

El servidor

Es aquí donde vamos a implementar el web service para que proporcione los siguientes servicios:

Esta parte la vamos a realizar en PHP.

El cliente

Una pequeña aplicación Web en Ruby on Rails que permita a los usuarios realizar los siguientes operaciones.

El nombre de mi juez en línea sera Micro OJ, por razones obvias.

Requisitos

El servidor

En esta parte vamos a implementar el Web Service. Antes que nada veamos nuestro modelo de base de datos.

Modelo de la base de datos

NOTA: Vamos a hacer una mezcla rara de ingles y español porque sino el profesor no va creer que el trabajo es nuestro.

El Web Service va a consistir de los siguientes servicios.

Servicios

create_problem Permite a los usuarios almacenar un problema y sus casos de prueba en la base de datos.
get_all_problems Recupera todos los problemas de la base de datos y se los retorna al cliente.
save_solution Recibe una solución por parte del cliente (código fuente).
compile_solution Compila una solución y retorna el resultado (compila o no).
get_last_error Retorna al cliente el error ocurrido al compilar una solución.
save_test_case Recibe un caso de prueba para un problema en específico y lo almácena en la base de datos.
test_solution Recibe el ID de una solución, la compila, la ejecuta con los casos de prueba correspondientes al problema y reporta los resultados al cliente (PASSO o FALLO).
create_user Crear un usuario en la BD.
get_user Retorna el ID y nombre de un usuario en particular.
find_user_by_remember_token Operación auxiliar para el inicio de sesión.
update_user_remember_token Operación auxiliar para el inicio de sesión.
get_user_data Recibe un usuario y contraseña y retorna los datos completos del usuario.
authenticate_user Verifica si un usuario existe en la base de datos con con el nombre de usuario y contraseña provista.
get_problem Retorna la descripción de un problema en particular (id, título y descripción).
get_all_users Retorna la lista de todos los usuario (únicamente el ID y el nombre).

PHP cuenta con soporte nativo para SOAP, pero nosotros vamos a emplear NuSOAP porque nos permite generar el archivo WSDL. El código que implementa el Web Service consta de aproximadamente de 800 líneas y por lo tanto no vamos a entrar en detalles de la implementación, sin embargo, el código no es complejo, con un conocimiento básico de PHP podrán entenderlo.

Definición de un servicio

Los servicios son funciones normales en PHP, la diferencia es que tenemos que registarlas, por ejemplo, veamos como crear el servicio create_problem.

Función:

function create_problem($user, $password, $title, $description, $author)
{
    if ($this->authenticate_user($user, $password) != VALID_USER) {
        return INVALID_USER;
    }

    $mysqli = $this->open_db();
    $query = $this->fips($title,
                         $description,
                         $author);

    $mysqli->query("SET NAMES UTF8");
    $mysqli->query($query);
    $code = $mysqli->insert_id;

    if ($code == 0) {
        if ($mysqli->errno == ER_DUP_ENTRY) {
            return ER_TITLE_ALREADY_TAKEN;
        } else {
            return ER_FAILED_TO_INSERT_RECORD;
        }
    }

    $mysqli->close();
    return $code;
}

Registro del servicio:

$server->register('Judge.create_problem',
    array(
        'user' => 'xsd:string',
        'password' => 'xsd:string',
        'title' => 'xsd:string',
        'description' => 'xsd:string',
        'author' => 'xsd:integer'),
    array('return' => 'xsd:integer'),
    'http://localhost/microoj_ws/judge.php'
);

El nombre del servicio comienza con Judge. porque la función es parte de la clase Judge. El primer argumento es el nombre del servicio, el segundo argumento es un arreglo especificando los datos que require el servicio, el tercer array es para especificar los datos que retorna el servicio, el último argumento es simplemente para documentación.

Crear un tipo de dato complejo

Para algunos de los servicios retornamos más de un dato, por ejemplo, get_problem y get_user_data que retornan una estructura con los datos del problema y del usuario respectivamente, o get_all_problems que retorna una lista de problemas. Veamos como crear un tipo de dato complejo y después un arreglo de objetos complejos.

$server->wsdl->addComplexType('Problem', 'complexType', 'struct', 'all', '',
    array(
        'id' => array(
            'type' => 'xsd:string',
            'minOccurs' => '1',
            'maxOccurs' => '1'),
        'title' => array(
            'type' => 'xsd:string',
            'minOccurs' => '1',
            'maxOccurs' => '1'),
        'description' => array(
            'type' => 'xsd:string',
            'minOccurs' => '1',
            'maxOccurs' => '1')
        )
    );

En este código especificamos el nombre del tipo de dato, los campos que contiene, los tipos de datos de cada campo, etc. Ahora un arreglo de objetos tipo Problem.

$server->wsdl->addComplexType(
    'ArrayOfProblem',
    'complexType',
    'array',
    'sequence',
    '',
    array(
        'problem' => array(
            'type'      => 'tns:Problem',
            'minOccurs' => '0', 
            'maxOccurs' => 'unbounded'
        )
    )
);

Y es así como se procede con el resto de los servicios.

El cliente

Esta la parte más extensa y que necesitará de mucha paciencia, hacer una descripción sería abrumador y aburrido, por lo que únicamente vamos a ver las partes más importantes.

Esta parte consume mucho tiempo principalmente porque hay que aprender el lenguaje Ruby y ademas como utilizar el framework Ruby on Rails, pero si ya conocen bien éstas dos herramientas supongo terminarán mucho más rápido. Mi recomendación para Ruby on Rails es Ruby on Rails Tutorial por Michael Hartl, el cual seguí para realizar el cliente.

Cliente SOAP

Para poder utilizar los servicios del Web Service será necesaria una herramienta que sea capaz de comunicarse con el servidor utilizando el protocolo SOAP, y esa herramienta se llama Savon, un cliente SOAP para el lenguaje Ruby.

Veamos como establecer comunicación con el servidor para crear un usuario.

require 'savon'

client = Savon::Client.new do
    # Dirección de nuestro servicio
    wsdl.document = "http://localhost/microoj_ws/judge.php?wsdl"
end

user_name = "usuario"
password = "1a2b3c"
request = client.request "Judge.create_user" do
    soap.body = {
        :name => user_name,
        :password => password
    }
end

response = request.to_hash[:judge_create_user_response]
puts response
insert_id = response[:return].to_i
puts insert_id

Al ejecutarlo debe arrojar algo similar a esto:

. Logs
.
.
{:return=>"4", :"@xmlns:ns1"=>"http://schemas.xmlsoap.org/soap/envelope/"}
4

El ejemplo anterior da por hecho que los servidores Apache y MySQL están encendidos y que la base de datos ya esta instalada.

NOTA: Con el código fuente incluyo el modelo de la base de datos, fue diseñado utilizando Workbench y por lo tanto podrán exportar la base de datos desde ahí.

La interfaz

Una imagen dice más que mil palabras, como todos sabemos, así que allá vamos.

Página principal de MicroOJ
Formulario de registro de usuarios
Formulario de inicio de sesión
Lista de problemas

Cada problema debe mostrar la opción de enviar una solución, sin embargo, nótese que en la imagen esta opción no figura, esto es porque el usuario no ha iniciado sesión y únicamente los usuarios registrados pueden enviar soluciones.

Usuario que ha iniciado sesión

En esta imagen observe que en la barra izquierda ha aparecido una nueva operación, “Crear problemas”, esta operación esta disponible únicamente para el usuario administrador.

Formulario para registrar un problema
Los usuarios pueden enviar soluciones
Formulario para enviar soluciones

El editor de código es Ace (http://ace.c9.io/).

Resultados de la solución

El diseño de la interfaz fue hecho utilizando Bootstrap (http://getbootstrap.com/2.3.2/index.html).

Desarrollo del cliente

Ahora que ya vimos como trabaja el cliente, es momento de explicar a grandes rasgos como se desarrollo.

Creación del proyecto

En este punto vamos a asumir que ROR ya esta instalado y configurado. Ejecuta los siguientes comandos para crear el proyecto.

$ mkdir microoj
$ cd microoj
$ rails new MicroOJ

El tercer comando genera el esquelo de nuestra aplicación.

Esqueleto de una aplicación ROR

La mayor parte de la acción ocurre en el subdirectorio app, especificamente en los subdirectorios app/views, app/controllers y app/models, esta estructura corresponde al patron de diseño Model View Controller.

Contenido de nuestro proyecto

Nuestra aplicación consiste de 5 vistas:

Cada vista tiene su respectivo controlador en la carpeta app/controllers:

Utilizamos también tres modelos para almacenar y manipular nuestros datos, estos archivos se ubican en la carpeta app/models:

Creación de un usuario

La alta de usuarios ocurre de la siguiente manera. El archivo app/views/users/new.html.erb contiene el diseño del formulario.

El código que se encarga de crear al nuevo usuario se encuentra en el archivo app/controllers/users_controller.rb. El método new se ejecuta cuando el usuario accede al formulario de registro, lo que hace es crear un objeto de tipo User, cuyos datos miembro estan enlazados a los campos del formulario.

def new
    @user = User.new
end

El método create se ejecuta en cuanto el usuario envía los datos de registro. Aquí se realiza la validación, y en caso de que la validación sea satisfactoria se procede a enviar los datos al servidor. En caso contrario se redirecciona nuevamente al formulario con el mensaje de error correspondiente.

def create
    @user = User.new(user_params)
    if @user.valid?
        client = Savon::Client.new do
            wsdl.document = WSDL_LOCATION
        end

        name = params[:user][:name]
        password = params[:user][:password]

        url = SecureRandom.urlsafe_base64
        remember_token = Digest::SHA1.hexdigest(url.to_s)
        request = client.request "Judge.create_user" do
            soap.body = {
                :user => name,
                :password => password,
                :remember_token => remember_token
            }
        end

        response = request.to_hash[:judge_create_user_response]
        id = response[:return].to_i

        if id > 0
            flash[:success] = "Bienvenido a Micro OJ"

            request = client.request "Judge.get_user_data" do
                soap.body = {
                    :user => name,
                    :password => password
                }
            end

            response = request.to_hash[:judge_get_user_data_response]
            user_data = response[:user]
            user = User.new
            user.id = user_data[:id]
            user.name = user_data[:id]
            user.password = user_data[:password]
            user.remember_token = user_data[:remember_token]

            sign_in user
            redirect_to root_path
        else
            render 'new'
        end

    else
        render 'new' # Pending
    end
end

Inicio de sesión

El diseño del formulario se encuentra en el archivo app/views/sessions/new.html.erb.

La código que se encarga de crear la sesión se encuentra en el archivo app/controllers/sessions_controller.rb, en el método create:

def create
    client = Savon::Client.new do
        wsdl.document = WSDL_LOCATION
    end

    request = client.request "Judge.authenticate_user" do
        soap.body = {
            :user => params[:session][:name],
            :password => params[:session][:password]
        }
    end

    answer = request[:judge_authenticate_user_response][:answer].to_i
    if answer == VALID_USER
        request = client.request "Judge.get_user_data" do
            soap.body = {
                :user => params[:session][:name],
                :password => params[:session][:password]
            }
        end

        response = request.to_hash[:judge_get_user_data_response]
        user_data = response[:user]
        user = User.new
        user.id = user_data[:id]
        user.name = params[:session][:name]
        user.password = params[:session][:password]
        user.remember_token = user_data[:remember_token]

        sign_in user
        redirect_to root_path
    else
        flash.now[:error] = 'Combinación usuario/contraseña invalida'
        render 'new'
    end
end

Cierre de sesión

Cerrar sesión básicamente equivale a destruir los datos del usuario actual para que ya no se pueda comunicar con el servidor como usuario registrado, esta acción se encuentra en el método destroy, en el mismo archivo del paso anterior.

def destroy
    sign_out
    redirect_to root_path
end

El método destroy llama a otro método, sign_out, el cual se encuentra en el archivo app/helpers/sessions_helper.rb, junto con otros métodos auxiliares para el manejo de sesiones.

def sign_out
    self.current_user = nil
    cookies.delete(:remember_token)
end

Creación de un problema

La lógica para crear un problema y enviarlo al servidor es la siguiente.

El archivo app/views/problems/new.html.erb contiene el diseño del formulario que se muestra en la figura .

El archivo app/controllers/problems_controller.rb contiene la lógica de la creación. El método new crea un objeto de tipo Problem, cuyos datos miembro estan enlazados a los campos del formulario.

def new
    @problem = Problem.new
end

Cuando el usuario envía el problema, el método create se encarga de realizar la conexión al servidor, enviar los datos y proveer una respuesta al usuario.

def create
    @problem_saved = false
    parameters = problem_params
    @problem = Problem.new(parameters)
    if not @problem.valid?
        render 'new'
    else
        client = Savon::Client.new do
            wsdl.document = WSDL_LOCATION
        end

        title = parameters[:title]
        description = parameters[:description]
        author = parameters[:author]

        request = client.request "Judge.create_problem" do
            soap.body = {
                :user => current_user.name,
                :password => current_user.password,
                :title => title,
                :description => CGI.escapeHTML(description),
                :author => author
            }
        end

        response = request.to_hash[:judge_create_problem_response]
        @insert_problem_code = response[:return]

        if @insert_problem_code.to_i > 0
            @problem_saved = true
            @problem_saved_id = @insert_problem_code
            @problem_title = parameters[:title]
            @problem_statement = parameters[:description]
            @problem_author = author

            @test_cases = []
            id = @insert_problem_code.to_i
            1.upto(TEST_CASES_PER_PROBLEM) do |tc|
                input_key  = "in_" + tc.to_s
                output_key = "out_" + tc.to_s
                request = client.request "Judge.save_test_case" do
                    soap.body = {
                        :user => current_user.name,
                        :password => current_user.password,
                        :id_problem => id,
                        :input => CGI.escapeHTML(params[input_key]),
                        :output => CGI.escapeHTML(params[output_key])
                    }
                end

                response = request.to_hash[:judge_save_test_case_response]
                test_id = response[:return].to_i
                @test_cases << {"id" => test_id, "in" => params[input_key],
                                "out" => params[output_key]}
            end

            render 'saved'
        else
            msg = "Ocurrio un error al intentar guardar el problema."
            @problem.errors.add("Error:", msg)
            render 'new'
        end

    end
end

Envio de una solución

El formulario para el envio de soluciones se localiza en el archivo app/views/solutions/new.html.erb. El controlador Solutions es el que se encarga de la lógica de esta operación:

def create
  @id_problem = params[:id_problem]
  @id_user = params[:id_user]
  @source_code = params[:source_code]
  @language = params[:language]

  client = Savon::Client.new do
      wsdl.document = WSDL_LOCATION
  end

  request = client.request "Judge.save_solution" do
      soap.body = {
          :user => current_user.name,
          :password => current_user.password,
          :id_problem => params[:id_problem],
          :source_code => params[:source_code],
          :language => params[:language]
      }
  end

  response = request.to_hash[:judge_save_solution_response]
  @submission_answer = response[:return]

  if @submission_answer[:error].to_i == SU_COMPILATION
      id = @submission_answer[:id].to_i
      request = client.request "Judge.test_solution" do
          soap.body = { 
              :user => current_user.name,
              :password => current_user.password,
              :id_solution => id,
          }
      end

      response = request.to_hash[:judge_test_solution_response]
      @test_results = response[:return][:item]
  else
      request = client.request "Judge.get_last_error"
      response = request.to_hash[:judge_get_last_error_response]
      @error_message = response[:message]
  end

  render 'submission_result'
end

Estas son las partes más importantes, creo que con esto podrán entender el resto de las operaciones.

Notas sobre el uso de la base de datos en ROR

Como habrán observado, empleamos tres tablas para almacenar nuestros datos, sin embargo, los datos nunca son almacenados en estas tablas ya que todo se envía al servidor remoto, esta es una alteración que he tenido que realizar para aprovechar las facilidades de Ruby on Rails pero a la vez cumplir con el requisito de emplear un Web service.

Al utilizar los modelos que proporciona Rails se nos facilita mucho la creación de formularios y la validación de los datos.

Si prescindiéramos del Web service y empleáramos las bases de datos nativas de Rails, la complejidad del cliente sería mucho menor.

Código fuente

El código tanto del cliente como del servidor están disponibles en Bitbucket en las siguientes direcciones:

O bien pueden clonar los repositorios desde la consola:

$ git clone https://rendon@bitbucket.org/rendon/microoj.git
$ git clone https://rendon@bitbucket.org/rendon/microoj_ws.git

La licencia de ambos proyectos es GPLv3, con excepción de los componentes empleados, los cuales tienen sus propias licencias.

Por hacer

Un sistema de juez en línea es más complejo de lo que se muestra en esta práctica, a continuación se listan algunas características deseables:

Referencias