Reporte II: MicroOJ & servicios Web
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:
- Crear un problema con sus casos de prueba y almacenarlos en una base de datos.
- Aceptar soluciones de parte de los usuarios y almacenarlas en la base de datos.
- Compilar y ejecutar la solución y reportar los resultados.
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.
- Crear problemas.
- Enviar soluciones (código fuente).
- Visualizar los resultados de la solución enviada.
El nombre de mi juez en línea sera Micro OJ, por razones obvias.
Requisitos
- Instalar y configurar un entorno LAMP
- Instalar y configurar Ruby on Rails
- El servidor debe tener instalados los compiladores para C, C++ y Python.
El servidor
En esta parte vamos a implementar el Web Service. Antes que nada veamos nuestro modelo de 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.
'savon'
require
Savon::Client.new do
client = # Dirección de nuestro servicio
"http://localhost/microoj_ws/judge.php?wsdl"
wsdl.document = end
"usuario"
user_name = "1a2b3c"
password = "Judge.create_user" do
request = client.request
soap.body = {:name => user_name,
:password => password
}end
:judge_create_user_response]
response = request.to_hash[
puts response:return].to_i
insert_id = response[ 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.
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.
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.
El editor de código es Ace (http://ace.c9.io/).
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.
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:
- problems Para crear y visualizar problemas.
- solutions Para enviar soluciones y visualizar resultados.
- users Crear y mostrar información sobre usuarios.
- sessions Para el inicio de sesión.
- static_pages Estructura y diseño del sitio, contenido estático.
Cada vista tiene su respectivo controlador en la carpeta app/controllers:
- problems_controller.rb
- sessions_controller.rb
- solutions_controller.rb
- static_pages_controller.rb
- users_controller.rb
Utilizamos también tres modelos para almacenar y manipular nuestros datos, estos archivos se ubican en la carpeta app/models:
- problem.rb
- solution.rb
- user.rb
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?
Savon::Client.new do
client = WSDL_LOCATION
wsdl.document = end
:user][:name]
name = params[:user][:password]
password = params[
SecureRandom.urlsafe_base64
url = Digest::SHA1.hexdigest(url.to_s)
remember_token = "Judge.create_user" do
request = client.request
soap.body = {:user => name,
:password => password,
:remember_token => remember_token
}end
:judge_create_user_response]
response = request.to_hash[:return].to_i
id = response[
if id > 0
:success] = "Bienvenido a Micro OJ"
flash[
"Judge.get_user_data" do
request = client.request
soap.body = {:user => name,
:password => password
}end
:judge_get_user_data_response]
response = request.to_hash[:user]
user_data = response[User.new
user = :id]
user.id = user_data[:id]
user.name = user_data[:password]
user.password = user_data[:remember_token]
user.remember_token = user_data[
sign_in user
redirect_to root_pathelse
'new'
render end
else
'new' # Pending
render 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
Savon::Client.new do
client = WSDL_LOCATION
wsdl.document = end
"Judge.authenticate_user" do
request = client.request
soap.body = {:user => params[:session][:name],
:password => params[:session][:password]
}end
:judge_authenticate_user_response][:answer].to_i
answer = request[if answer == VALID_USER
"Judge.get_user_data" do
request = client.request
soap.body = {:user => params[:session][:name],
:password => params[:session][:password]
}end
:judge_get_user_data_response]
response = request.to_hash[:user]
user_data = response[User.new
user = :id]
user.id = user_data[:session][:name]
user.name = params[:session][:password]
user.password = params[:remember_token]
user.remember_token = user_data[
sign_in user
redirect_to root_pathelse
:error] = 'Combinación usuario/contraseña invalida'
flash.now['new'
render 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_pathend
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
:remember_token)
cookies.delete(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?
'new'
render else
Savon::Client.new do
client = WSDL_LOCATION
wsdl.document = end
:title]
title = parameters[:description]
description = parameters[:author]
author = parameters[
"Judge.create_problem" do
request = client.request
soap.body = {:user => current_user.name,
:password => current_user.password,
:title => title,
:description => CGI.escapeHTML(description),
:author => author
}end
:judge_create_problem_response]
response = request.to_hash[@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 = []
@insert_problem_code.to_i
id = 1.upto(TEST_CASES_PER_PROBLEM) do |tc|
"in_" + tc.to_s
input_key = "out_" + tc.to_s
output_key = "Judge.save_test_case" do
request = client.request
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
:judge_save_test_case_response]
response = request.to_hash[:return].to_i
test_id = response[@test_cases << {"id" => test_id, "in" => params[input_key],
"out" => params[output_key]}
end
'saved'
render else
"Ocurrio un error al intentar guardar el problema."
msg = @problem.errors.add("Error:", msg)
'new'
render 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]
Savon::Client.new do
client = WSDL_LOCATION
wsdl.document = end
"Judge.save_solution" do
request = client.request
soap.body = {:user => current_user.name,
:password => current_user.password,
:id_problem => params[:id_problem],
:source_code => params[:source_code],
:language => params[:language]
}end
:judge_save_solution_response]
response = request.to_hash[@submission_answer = response[:return]
if @submission_answer[:error].to_i == SU_COMPILATION
@submission_answer[:id].to_i
id = "Judge.test_solution" do
request = client.request
soap.body = { :user => current_user.name,
:password => current_user.password,
:id_solution => id,
}end
:judge_test_solution_response]
response = request.to_hash[@test_results = response[:return][:item]
else
"Judge.get_last_error"
request = client.request :judge_get_last_error_response]
response = request.to_hash[@error_message = response[:message]
end
'submission_result'
render 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:
- Mejorar el manejo de usuarios. Que los usuarios puedan ver el perfil de los demás participantes y ver su progreso.
- Reportar estadísticas de cada usuario, como son, problemas resueltos, número de intentos, cuantos de ellos ha sido aceptados, cuantos rechazados, etc. Toda esta información debería ser mostrada en el perfil del usuario.
- Implementar un sistema de Ranking, en mi experiencia, esto es un factor de motivación.
- RETO: Soportar concursos en tiempo real.