Logo Pablo Monteserín

Pablo Monteserín

   Cursos de páginas web

  • Quién soy
  • Portfolio
  • Servicios
  • Blog
  • Contactar
  • Mi cuenta
  • Login

Curso de MVC

Ejercicios

Ejercicios

Ejercicios

Recursos

Ejercicios

Videos

A lo largo del curso encontrarás estos iconos, que son enlaces a los diferentes recursos del curso (ejercicios, ejemplos, ficheros descargables y videos premium).

Índice del curso de MVC

  1. Modelo Vista Controlador (modelo 2)
  2. Grid Class
  3. Procesar toda la operativa en doGet o doPost
  4. Definir constantes de aplicación en el web.xml
  5. Ejercicio - Lista de invitados
  6. Ejercicio libreria
  7. Ejercicio fútbol
  8. Trabajo con fechas
  9. Hospital I
  10. Listado libros
  11. Ejercicio calidades
  12. Sesión
  13. Listener de sesión
  14. Ejercicio Login
  15. Ejercicio mensajeria
  16. Ejercicio foro
  17. Contexto
  18. Upload file
  19. Web Services

Modelo 1

Se usaban Servlets, JSP y custom Tags, pero sin utilizar una estructura bien definida.

Modelo Vista Controlador (modelo 2)

  • Vista: Son las pantallas que interaccionan con el usuario (la interfaz del usuario) (html y jsp). Todo lo que tenga que ver con la vista se almacena en la carpeta WEB-CONTENT.
  • Controlador: Recibe las peticiones de la vista y se la manda al modelo (servlets).
  • Modelo: Es la parte que se comunica con el servidor (consulta la base de datos, manda un correo, etc.)Son ficheros Java o EJB. En él, no debe haber ninguna referencia al protocolo http, ni request, ni response, ni session, etc.
ejemplo del flujo de vida de una aplicación que implementa el paradigma del modelo-vista-controlador

Cuando un proyecto comienza a crecer...

El modelo se almacena en un proyecto java común, y la vista en un proyecto web.

Saludar con MVC

Descargar Workspace

entrada.jsp<form action="ControllerServlet">
	<input type="text" name="nombre">	
	<input type="submit">
</form>
ControllerServlet.java
protected void doPost(HttpServletRequest, HttpServletResponse) throws ServletException, IOException{
	String resultado = Servicio.saludar(request.getParameter("nombre"));
	request.setAttribute("resultado", resultado);
	request.getRequestDispatcher("salida.jsp").forward(request, response);
}		
	
Servicio
public class Servicio{
	public static String saludar(String nombre){
		return "hola" + nombre;
	}
}
	

salida.jsp
<%= request.getAttribute("resultado") %>
	

Ejercicio calculadora

Hacer una calculadora siguiendo el patrón MVC. Si el resultado es mayor que 1000, el controlador redirigirá a una página diferente en la que se felicitará al usuario.

ejemplo de como debería quedar el ejercicio de la calculadora con Java

Solventando problemas de codificación

Configuraremos el cliente para recibir y enviar la información en UTF-8.

			
<%@ page language="java" contentType="text/html; charset=utf-8"%>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
			
		

Definiendo la codificación de todas las peticiones

Cuando se envía la petición por POST, el servidor TOMCAT no es capaz de saber cuál es el formato de codificación del cliente. Como posible solución está crear un filtro para que todas las peticiones pasen por él y dónde se especifique que sean en formato UTF-8.

			
UTF8Filter.java
public class UTF8Filter implements Filter {
	private String encoding;
	//Recogemos el tipo de codificación definido en el web.xml
	public void init( FilterConfig filterConfig ) throws ServletException {
		encoding = filterConfig.getInitParameter( "requestEncoding" );
	}
	// Metemos en la request el formato de codificacion UTF-8
	public void doFilter( ServletRequest request, ServletResponse response, FilterChain fc )throws IOException, ServletException {
		request.setCharacterEncoding( encoding );
		fc.doFilter( request, response );
	}
	public void destroy() {}
}

web.xml
…
<filter>
	<filter-name>UTF8Filter</filter-name>
	<filter-class>com.pablomonteserin.filter.UTF8Filter</filter-class>
          <init-param>
		<param-name>requestEncoding</param-name>
		<param-value>UTF-8</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>UTF8Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
			
		

Configurando el servidor para trabajar con UTF8

En Tomcat por defecto se especifica el formato de codificación ISO-8859-1. Para cambiar la codificación tenemos que modificar el archivo server.xml que se encuentra en DIRECTORIO_INSTALACION_TOMCAT\conf\server.xml.

Añadimos el atributo URIEncoding=“UTF-8” en la etiqueta <Connector port="8080" … />

<Connector port="8080" maxHttpHeaderSize="8192" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" connectionTimeout="20000" disableUploadTimeout="true" URIEncoding="UTF-8"/>

Modelo 5 capas

Vista.
La apariencia de la página. Son los .jsp, .html, .xhtml, etc.
→ Controlador.
Recoge la información de la vista y se la manda al BO.
→
BO.
Contiene las llamadas a cada una de las pequeñas operaciones independientes que componen la operación que queremos realizar. En el caso de estar realizando operaciones contra una base de datos, el el BO abrimos y cerramos la conexión con la base de datos. Llama al DAO.
→
DAO
En él haces operaciones contra sistemas ajenos a nuestra aplicación (una base de datos, un web service, etc.). También se le llama capa de integración.
Son las clases 'tontas'. Contienen métodos nucleares que realizan operaciones concretas.
→
Base de datos

Según las recomendaciones de la JSR (Java Specification Request) y de la JCP (Java Comunity Proccess), si los casos de uso están bien definidos habrá un DAO por cada BO y un BO por cada Action.

Recomendación: no usar métodos estáticos para llamar al BO y al DAO

Las variables definidas dentro de un método estático son compartidas por todos los hilos que llamen al método.

Por tanto:
Si tenemos un método estático consultarPaciente(int id), y dos usuarios distintos que simultáneamente llaman a este método, puede ocurrir lo siguiente:
el primer usuario consulta para la id 4 y obtiene un paciente, pero antes de que llegue al return del método, llega el segundo usuario y consulta para la id 7, modificando la variable paciente que almacena el resultado de la consulta y que es común para ambos. Por tanto, el primer usuario, obtendrá el paciente que consulta el segundo.

Ni las llamadas al BO, ni las llamadas al DAO deben ser hechas mediante métodos estáticos.

Procesar toda la operativa en doGet o doPost

Desde el método doGet podemos llamar a:
doPost(request,response);

y desde el método doPost podemos llamar a:
doGet(request,response);

Patrones de diseño → Singleton

Un patrón de diseño es una solución a un problema concreto en el desarrollo de software.

Definir constantes de aplicación en el web.xml

<context-param>
	<param-name>foo</param-name>
	<param-value>bar</param-value>
</context-param>
<%=getServletContext().getInitParameter("foo") %>
${initParam.foo}

Ejercicio - Lista de invitados

Hacer una página web para una lista de invitados con 4 secciones. Una para consultar los invitados, otra para dar de alta un nuevo invitado y otra para darlo de baja.La tabla que usaremos tendrá dos campos: nombre (VARCHAR) e ID (INT, AUTOINCREMENT, PK).

Cada uno de los siguientes pantallazos representa una página jsp diferente.

lista invitados

Ejercicio fútbol

La segunda carga una collection de beans de Equipo, y la tercera carga una collection de beans de Jugador en función de un parámetro (EQUIPO_COD) que fue procesado en el controlador.

Descargar base de datos.

Descargar imágenes.

Entidades para usar en Hibernate

Pantallazo ejercicio resuelto

Trabajo con fechas

Paso de String a Date:

	
SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd");
Date fecha = s.parse(request.getParameter("fechaAlta"));
	

Paso de Date a String:
	
SimpleDateFormat s = new SimpleDateFormat("dd-MM-yyyy"); 
String fechaAlta = s.format(fecha); //fecha es un objeto de tipo Date
	

Descomposición de una fecha en formato String:
	
String fecha = ''24-07-1982'';
String [] fechaSplitada = fecha.split(''-'');
String dia = fechaSplitada[0];
String mes = fechaSplitada[1];
String anio = fechaSplitada[2];
	

Paso de Date a GregorianCalendar:

	
GregorianCalendar gc = new GregorianCalendar();
gc.setTime(fecha); //fecha es un objeto de tipo Date
String dia = gc.get(GregorianCalendar.DAY_OF_MONTH);
String mes = gc.get(GregorianCalendar.MONTH)+1;
String anio = gc.get(GregorianCalendar.YEAR);		
	

Construcción de un objeto GregorianCalendar a partir del día, mes y año por separado:

	
new GregorianCalendar(int year, int month, int dayOfMonth)
	

Nota:
Para pasar de String a GregorianCalendar hay que pasar previamente de String a Date.

Etiqueta JSTL para formato de fechas

		

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<fmt:formatDate pattern=“dd-MM-yyyy“ value=“${fecha_java_util_Date}“ />

<fmt:formatDate pattern=“dd“ value=“${fecha_java_util_Date}“ />
		
	

<input type="hidden" name="action" value="modificar" />

Una etiqueta <input type="hidden" /> nos permite enviar información en un formulario sin que dicha información aparezca en la vista.

Hospital

Hospital I

La base de datos tendrá 4 campos: id(PRIMARY KEY, AUTOINCREMENT), nombre (VARCHAR), apellidos (VARCHAR), fecha_alta(DATE).

Para convertir una fecha en un objeto de tipo Date:

	
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
Date fecha = formatter.parse(stringFecha);

//MM mayúscula, las minúsculas son para minutos.
	
Estas conversiones las haremos en el controlador (No modificaremos el bean paciente para añadirle las propiedades día, mes y año)

Para modificar la fecha primero convierto el tipo de dato recibido del formulario a un dato de tipo java.util.Date.

Luego, cuando le pase parámetros a la consulta sql convertiré esta fecha un dato de tipo java.sql.Date:

	
pstmt.setDate(3,new java.sql.Date(fecha.getTime()));
	
pantallazo ejercicio hospital I

Hospital II

Necesitamos que el formulario de modificación haga dos cosas: actualice la base de datos y refresque los datos de los cuadros de texto. Para ello, nos vendrá bien el siguiente código javascript:

		
<script type="text/javascript">
function cambiarAction(){
        document.getElementById("action").value="actualizaCampos";
        document.getElementById("formulario").submit();
}
</script>
		
	
pantallazo ejercicio hospital II

Ejercicio listado libros

Ejercicio listado libros I

Pantallazo ejercicio listado libros 1

Ejercicio listado libros II

pantallazo listado libros

Nota:
Pasarle parámetros al action de un formulario directamente en su URL funciona sólo si estamos enviando la información por POST. En caso contrario, la URL que definimos en el action y la que generamos dinámicamente entran en conflicto y los parámetros definidos explícitamente en el action del formulario no son enviados.

	
<form method="post" action="ServletController?action=alta">
	

Para programar la opción de agregar un nuevo registro, tenemos dos opciones:

  • Podemos hacer un formulario que abarque sólo las celdas de la fila dónde se encuentra el botón de agregar (esta es la opción más sencilla, pero desde el punto de vista de la validación del código html sería incorrecta).
  • Podemos hacer un formulario que abarque toda la tabla html donde se están mostrando los resultados de la consulta (esta opción es un poco más compleja, pero el código html validaría correctamente).

Ejercicio listado libros III

Pantallazo ejercicio listado libros

Lo más sencillo será utilizar un formulario para cada fila, de tal forma que abarque todas las celdas de cada registro.Esta opción no valida el código html.

Ejercicio listado libros IV

Pantallazo ejercicio listado libros 4

Podemos usar la siguiente función:

	
function modificar(id){
	document.getElementById("action").value = "modificar";
	document.getElementById("idEnviada").value = id;
	document.getElementById("modifica_titulo").value = document.getElementById("titulo_"+id).value;
	document.getElementById("modifica_precio").value = document.getElementById("precio_"+id).value;
	document.getElementById("formulario").submit();
}
	

Ejercicio calidades

Pantallazo listado jugadores

Para que las capas amarillas contenidas en la tabla aparezcan alineadas con la parte baja de la misma usaremos el siguiente estilo: <td style='vertical-align:bottom'>

Como utilizando JSTL y un solo bucle es posible generar la tabla de jugadores de la izquierda y la tabla de calidades de la derecha.

	
<c:forEach var="jugador" items="${requestScope.jugadores}">
	<tr><td><c:out value="${jugador.numero_camiseta}" /></td><td><c:out value="${jugador.nombre}" /></td></tr>
	<c:set var="fsup">
		${fsup}<td style="vertical-align:bottom;">
		<div style="background-color:yellow; width:20px; height:<c:out value="${jugador.calidad*30}" />px"></div></td>		
	</c:set>
	<c:set var="finf">
		${finf}<td><c:out value="${jugador.numero_camiseta}" /></td>
	</c:set>	
</c:forEach>	
	

Ejercicio librería con sesión

Repetir el ejercicio de la librería cargando mediante un SessionListener los datos de la base de datos en un ArrayList que almacenaremos en la sesión. A partir de ahí, todas las operaciones las haremos contra dicha sesión.

En caso de terminar antes, intentar que los cuadros de modificación se actualicen al cambiar el valor de la combo.
Para ello, cuando la combo cambie, enviaré la id del libro, la compararé con la id de los libros almacenados en el arraylist y devolveré los datos del libro para el cual hubo coincidencia.

librería mvc librería mvc 2

Ejercicio librería – con Map

Repetir el ejercicio de la diapositiva anterior utilizando un TreeMap para almacenar la información en vez de un ArrayList.


Métodos del TreeMap:
objetoTreeMap.put(Object key, Object value);
objetoTreeMap.remove(Object key);
objetoTreeMap.get(Object key);
objetoTreeMap.firstEntry().getValue();

Recorrer un Map:
<c:forEach var="libro" items="${sessionScope.libros}">
	<option value="<c:out value="${libro.value.id}" />" 
...
	

Ejercicio Login

login

Si el usuario se loguea con éxito, colocamos un bean del usuario logueado en la sesión. Y redirigimos a la página de ''usuario logueado'. En caso contrario, redirigimos a la página de''se ha producido un error''.

Ejercicio Mensajería

ejercicio mensajería
mensaje
id remitente destinatario mensaje
1 1 1 Hola!
2 1 2 Qué pasa tron!
3 1 1 Hola Caracola
4 1 2 Te odio
usuario
id nombre pass
1 pp kk
2 kk kk
		
<input type="checkbox" name="arrayDeUsuarios" value="<c:out value="${usuario.id}"/>"/>
String[] arrayDeUsuarios = request.getParameterValues("arrayDeUsuarios");
		
	

Ejercicio foro

ejercicio foro
usuario
id nombre pass
1 pp pp
2 kk kk
comentario
id id_usuario id_hilo comentario
1 2 7 es super guay!
2 2 8 En pablomonteserin.com los vendes muy buenos
hilo
id id_usuario nombre_hilo texto_hilo
7 1 Escoger ordenador Dónde comprar uno bueno
8 1 ¿Qué opinas de Ubuntu? Dudas sobre Ubuntu

Context

Hay un solo Context por cada aplicación web que hay en el servidor.

Lo que es propio de cada usuario va en sesión. Lo que es común a todos va en Context.

En un carrito de compra, el carrito de cada usuario se almacenaría en una variable sesión, mientras que la lista de precios estaría en una variable Context.

Variables de contexto

Acceder al contexto en el servlet:

	
ServletContext context = request.getSession().getServletContext();
	

Acceder al contexto en el listener:

	
ServletContext context = arg0.getServletContext();
	

Serialización

Consiste en convertir un objeto en una sucesión de bits o en un formato humanamente más legible como XML o JSON, entre otros.

La serialización es un mecanismo ampliamente usado para transportar objetos a través de una red, para hacer persistente un objeto en un archivo o base de datos, o para distribuir objetos idénticos a varias aplicaciones o localizaciones.

EscribirDatoMain

	
Persona p = new Persona();
SerializeObjecs.writeObject(p,"/home/monty/Documents/serializado.txt" )
	

LeerDatoMain

	
Persona p = (Persona)SerializeObjects.readObject("/home/monty/Documents/serializado.txt")
	

Ejercicio suma sesión y contexto

Hacer un jsp con dos campos, n1 y n2. Al pulsar en el botón sumar, el controlador procesará:

  • la suma de request.
  • la suma de sesión.
  • la suma de contexto.

Haremos todas las operaciones en el controlador.

Con un listener, cuando baje el servidor guardaré la información de la suma del contexto en un archivo de texto. Al subir el servidor se recupera la información del archivo de texto.

Descargar SerializeObjects.java

Ejercicio

Repetir el ejercicio de las calidades almacenando los equipos y los jugadores en el contexto. Almacenaré los equipos como un TreeMap y los jugadores como una Collection. Podré recuperar los jugadores de un equipo mediante un método getJugadores() de la clase Equipo. Dicho método devuelve una collection de jugadores.

El código de la derecha es parte del código que irá en el ContextListener para almacenar los jugadores en el contexto.

		
ArrayList jugadores = new ArrayList();
ResultSet rsJugadores = stm.executeQuery("select * from jugador order by equipo_cod, numero_camiseta");
boolean haySiguienteRegistro=rsJugadores.next();
Integer equipo_cod_actual = 1;
Integer equipo_cod_siguiente = 1;
while(haySiguienteRegistro){
	Jugador jugador = new Jugador();
	equipo_cod_actual = rsJugadores.getInt("equipo_cod");		    			
	jugador.setEquipo_cod(equipo_cod_actual);
	jugador.setJugador_cod(rsJugadores.getString("jugador_cod"));
	...
	jugadores.add(jugador);
	haySiguienteRegistro = rsJugadores.next();
	if(haySiguienteRegistro){
	equipo_cod_siguiente = rsJugadores.getInt("equipo_cod");		    			
	if(rsJugadores.getInt("equipo_cod") != equipo_cod_actual){
		equipos.get(equipo_cod_actual).setJugadores(jugadores);	
		jugadores = new ArrayList();
	}
	}else{
		equipos.get(equipo_cod_actual).setJugadores(jugadores);	
	}
}
ServletContext context = arg0.getServletContext();
context.setAttribute("equipos", equipos);
		
	

Upload file (subir fichero)

	
index.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head><title>Insert title here</title></head>
<body>
<form method="POST" enctype='multipart/form-data' action="ServletController">
 Por favor, seleccione el trayecto del fichero a cargar<br/>
<input type="file" name="fichero">
<input type="submit">
</form> 
</body>
</html>

ServletController.java
public boolean procesaFicheros(HttpServletRequest req) {
try {
	DiskFileUpload fu = new DiskFileUpload();// construimos el objeto que es capaz de parsear la pericion
	fu.setSizeMax(1024*512); // máximo numero de bytes (512)
	fu.setSizeThreshold(4096);// tamaño por encima del cual los ficheros son escritos directamente en disco
	fu.setRepositoryPath("/tmp");// directorio en el que se escribirán los ficheros con tamaño superior al soportado en memoria
	List fileItems = fu.parseRequest(req);// ordenamos procesar los ficheros
	if(fileItems == null) {
		System.out.println("La lista es nula");
		return false;
	}
	Iterator i = fileItems.iterator();// Iteramos por cada fichero
	FileItem actual = null;
	while (i.hasNext()){
		actual = (FileItem)i.next();
		String fileName = actual.getName();
		File fichero = new File(fileName);// construimos un objeto file para recuperar el trayecto completo
		System.out.println("El nombre del fichero es " + fichero.getName());// nos quedamos solo con el nombre y descartamos el path
		fichero = new  File("/home/monty/Desktop/"+fichero.getName());// escribimos el fichero colgando del nuevo path
		actual.write(fichero);
	}
}catch(Exception e) {
	System.out.println("Error de Aplicación " + e.getMessage());
	return false;
}
return true;
}

	

Web Service

  1. Creamos un Dynamic Web Proyect webService_llamado.
  2. Creamos un paquete dentro del proyecto.
  3. Creamos una clase llamada ClaseLlamada (llámala cómo quieras, pero no igual que el método) dentro de este proyecto con el siguiente método estático, que recibe un parámetro y devuelve el parámetro transformado.
    				
    		public static String saludar(Strin parametro){
    			return "Hola " + parametro;
    		}
    				
    			
  4. Para dar este paso es posible que sea necesario que el servidor esté arrancado, ya que a veces no se arranca automáticamente. Botón derecho sobre la clase recién creada → Web Service → Create Web Service → next → elijo los métodos que quiero publicar como web service(deberían ser métodos que devolviesen algo) → finish
  5. Creo un nuevo dynamic web proyect llamador.
  6. Creo un paquete dentro del src.
  7. Botón derecho sobre el paquete recién creado → new → web service client → browse → busco el fichero wsdl (webcontent/wsdl/saludar.wsdl) creado en el proyecto anterior → finish
  8. Creo una clase Main desde la que llamo al método que contiene la clase
    	
    public static void(String [] args){
    	ClaseLlamadaProxy claseLlamadaProxy = new ClaseLlamadaProxy();
    	try{
    		System.out.println(claseLlamadaProxy.saludar("Juan"));
    	}catch(Remote Exception){
    		e.printStackTrace();
    	}
    }
    	
    
Notas:
El proyecto llamado debe estar desplegado en el servidor cuando lo llamemos.

Aviso Legal

Los derechos de propiedad intelectual sobre el presente documento son titularidad de D. Pablo Monteserín Fernández Administrador, propietario y responsable de pablomonteserin.com. El ejercicio exclusivo de los derechos de reproducción, distribución, comunicación pública y transformación pertenecen a la citada persona.

Queda totalmente prohibida la reproducción total o parcial de las presentes diapositivas fuera del ámbito privado (impresora doméstica, uso individual, sin ánimo de lucro).

La ley que ampara los derechos de autor establece que: “La introducción de una obra en una base de datos o en una página web accesible a través de Internet constituye un acto de comunicación pública y precisa la autorización expresa del autor”.

El contenido de esta obra está protegido por la Ley, que establece penas de prisión y/o multa, además de las correspondientes indemnizaciones por daños y perjuicios, para quienes reprodujesen, plagiaren, distribuyeren o comunicaren públicamente, en todo o en parte, o su transformación, interpretación o ejecución fijada en cualquier tipo de soporte o comunicada a través de cualquier medio.

El usuario que acceda a este documento no puede copiar, modificar, distribuir, transmitir, reproducir, publicar, ceder, vender los elementos anteriormente mencionados o un extracto de los mismos o crear nuevos productos o servicios derivados de la información que contiene.

Cualquier reproducción, transmisión, adaptación, traducción, modificación, comunicación al público, o cualquier otra explotación de todo o parte del contenido de este documento, efectuada de cualquier forma o por cualquier medio, electrónico, mecánico u otro, están estrictamente prohibidos salvo autorización previa por escrito de Pablo Monteserín.

El autor de la presente obra podría autorizar a que se reproduzcan sus contenidos en otro sitio web u otro soporte (libro, revista, e-book, etc.) siempre y cuando se produzcan dos condiciones:

  1. Se solicite previamente por escrito mediante email al correo pablomonteserin@pablomonteserin.com.
  2. En caso de aceptación, no se modifiquen los textos y se cite la fuente con absoluta claridad.

Una parte de las imágenes utilizadas en este documento no son propiedad de Pablo Monteserín, por lo que, si alguna de estas imágenes estuviera sujeta a derechos de autor, o a algún otro tipo de derecho que impida su publicación en este documento, una vez que el autor, Pablo Monteserín, tenga conocimiento del hecho, procederá a la retirada inmediata de la imagen protegida por los derechos pertinentes.

Recibe las últimas noticias

Revisa tu bandeja de entrada o la carpeta de spam para confirmar tu suscripción.

Otros

  • Testimonios
  • Recursos
  • Aviso Legal

Redes sociales

  • Facebook
  • YouTube
  • Twitter
  • LinkedIn

Accede a tu cuenta

Para ver los videos necesitas acceder a tu Cuenta Premium.

  • Olvidé mi contraseña
  • Registrar una cuenta premium que te dará acceso a los videos de todos los cursos