Spring Boot - DataRest conexión con MySQL y manejo con JPA

La mayoría de las configuraciones que se muestran en la red, incluida la configuración de spring, viene algo confusa, así que aquí hay una configuración simple a MySQL y se exponen los datos con ayuda de DataRest.

 

1. Configuración Maven

<groupId>com.ledze.springboot</groupId>
<artifactId>ledze-springboot-mysql</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>ledze-springboot-mysql</name>
<description>Spring Boot demo to connect to a DB MySQL and DataRest</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>spring-boot-starter-jpa</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>RELEASE</version>
<scope>runtime</scope>
</dependency>

 

2. Configuración Properties

spring.datasource.url= jdbc:mysql://localhost:3306/ledzeschemamysqldb
spring.datasource.username= ledzeuserdb
spring.datasource.password= 123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
server.port=
8001

 

3. Código Java - Entidad

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Dec/2016.
*/
@Entity
public class Usuario {


@Id
@GeneratedValue
private Long id;


private String nombre;

private String apaterno;

private String amaterno;

private String email;

private String password;

public Usuario() {//seguimos sin saber por qué jpa requiere uno de estos
}


public Usuario(String nombre, String apaterno, String amaterno, String email) {
this.nombre = nombre;
this.apaterno = apaterno;
this.amaterno = amaterno;
this.email = email;
this.password = RandomStringUtils.randomAlphanumeric(5).concat(
RandomStringUtils.randomAscii(3)
);
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {
this.nombre = nombre;
}

public String getApaterno() {
return apaterno;
}

public void setApaterno(String apaterno) {
this.apaterno = apaterno;
}

public String getAmaterno() {
return amaterno;
}

public void setAmaterno(String amaterno) {
this.amaterno = amaterno;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {

return "Usuario{" +
"id=" + id +
", nombre='" + nombre + '\'' +
", apaterno='" + apaterno + '\'' +
", amaterno='" + amaterno + '\'' +
", email='" + email + '\'' +
'}';
}
}

 

4. Código Java - Interfaz que habla con la BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Dec/2016.
*/
@RepositoryRestResource
public interface RepositorioUsuariosDB extends JpaRepository<Usuario, Long> {

Collection<Usuario> findByEmail(@Param("m") String email);
}

 

5. MySQL - Tabla usuarios en la base de datos.

 

Este es el contenido de la tabla de usuarios en la base de datos de MySQL

 

6. Browser - Consulta de datos en web, expuestos por DataRest.

 

 

Te puedes descargar mi código directamente del repositorio en github o en formato zip.

 

 

Add a comment

Spring Boot - DataRest RESTful MVC Thymeleaf. Extrabonus (Actuator y Remote Shell)

- Prerequisitos

Genera tu proyecto con Spring Boot Initializr.

- spring-boot-starter-data-jpa

- spring-boot-starter-data-rest

- spring-boot-starter-thymeleaf

- h2

- spring-boot-starter-actuator

- spring-boot-starter-remote-shell

 

1. Configuración Maven.

<groupId>com.ledzedev.springboot.datarest</groupId>
<artifactId>ledzedev-springboot-datarest-restfull-mvc-thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>ledzedev-springboot-datarest-restfull-mvc-thymeleaf</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-remote-shell</artifactId>
</dependency>
</dependencies>

2. Entidad para escribir en BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 14/Nov/2016.
*/
@Entity
class CuentaBancaria {


@Id
@GeneratedValue
private Long id;


private String titularCuenta;

private String saldoDisponible;

public CuentaBancaria(String titularCuenta, String saldoDisponible) {
this.titularCuenta = titularCuenta;
this.saldoDisponible = saldoDisponible;
}

public CuentaBancaria() {//JPA requiere constructor vacío y nadie sabe por que???
}


public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitularCuenta() {
return titularCuenta;
}

public void setTitularCuenta(String titularCuenta) {
this.titularCuenta = titularCuenta;
}

public String getSaldoDisponible() {
return saldoDisponible;
}

public void setSaldoDisponible(String saldoDisponible) {
this.saldoDisponible = saldoDisponible;
}

@Override
public String toString() {

return "CuentaBancaria{" +
"id=" + id +
", titularCuenta='" + titularCuenta + '\'' +
", saldoDisponible=" + saldoDisponible +
'}';
}
}

 

3. Interfaz que hable con la BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 14/Nov/2016.
*/
@RepositoryRestResource
public interface RepositorioCuentas extends JpaRepository<CuentaBancaria, Long> {


CuentaBancaria findById(@Param("id") Long id);

Collection<CuentaBancaria> findByTitularCuenta(@Param("tc") String titularCuenta);

}

4. Controlador MVC (envía información a Thymeleaf).

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 14/Nov/2016.
*/
@Controller
public class ControladorMvcRestCuentaBancaria {


@Autowired
private RepositorioCuentas repositorioCuentas;


@RequestMapping("/cuentasBancarias.aspx")
String estadoDeCuenta(Model model){

model.addAttribute("cuentas", this.repositorioCuentas.findAll());

return "estadoDeCuenta";
}

}

 

5. Configuración carga inicial de la aplicación: CommandLineRunner.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 14/Nov/2016.
*/
@Bean
CommandLineRunner runner(RepositorioCuentas repositorioCuentas){

return x -> {
Arrays.asList(new CuentaBancaria("Gerardo", "1000.00"),
new CuentaBancaria("Ledze", "2000.00"),
new CuentaBancaria("Maribel", "3000.00"),
new CuentaBancaria("Santiago", "4000.00"),
new CuentaBancaria("Yara", "5000.00"),
new CuentaBancaria("Esperanza", "6000.00"),
new CuentaBancaria("Paolo", "7000.00"),
new CuentaBancaria("Sio", "8000.00"),
new CuentaBancaria("Angel", "9000.00")
)
.forEach(repositorioCuentas::save);
log.info("Carga inicial de cuentas bancarias terminada.");
};
}

6. Plantilla HTML con soporte Thymeleaf.

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">

<head>
<title>Ejemplo RESTful viejito en MVC con Thymeleaf</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>ESTADO DE CUENTA</h1>

<div th:each="cb : ${cuentas}">
<div>
<b th:text="${cb.id}">ID</b>
<span th:text="${cb.titularCuenta}">Titular de la cuenta</span>
<span th:text="${cb.saldoDisponible}">Saldo disponible</span>
</div>
</div>

</body>
</html>

7. Resultado en web (en mi caso Google Chrome).

- Además de Thymeleaf, Spring Boot tiene soporte para otros frameworks de plantillas como Handlebars, Velocity, etc.

 

Extra bonus (Actuator y Remote Shell)

Como resultado de agregar spring-boot-starter-actuator, se publican una serie de servicios que entregan información útil en un ambiente productivo.

- Actuator

 

- Autoconfig

 

- Beans

 

- Configprops

 

- Dump

 

- Env

 

- Health

 

- Mappings

 

- Metrics

 

- Trace

 

- Remote Shell

Presenta las mismas opciones pero en una consola remota. Únicamente hay que agregar al pom.xml la dependencia "spring-boot-starter-remote-shell".

La contraseña de acceso es generada aleatoriamente al arrancar la aplicación:

La consola se ve así:

 

 Te puedes descargar mi código directamente de mi repositorio en github o te los puedes descargar en formato zip.

 

Artículos Relacionados

- Spring Boot Initializr

Documentación Original

- Spring Boot

- Spring Boot Actuator

- Spring Boot Remote Shell

 

Add a comment

Spring Boot - Servicio RESTful con Base de Datos

- Prerequisitos

Genera tu proyecto con Spring Boot INITIALIZR.

- Spring boot starter test.

- Spring boot starter data JPA.

- H2 como manejador de BD (esta en particular vive en memoria y solo es para esta cápsula). Puedes utilizar la BD que soporte Spring.

- Spring boot starter web.

 

 

1. Configuración MAVEN.

<groupId>com.ledzedev.springboot</groupId>

<artifactId>demo-spring-boot-restful</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>demo-spring-boot</name>
<description>Proyecto demo de un Servicio RESTful con Base de datos y Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

2. Agregamos la entidad que vamos a insertar en la BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Nov/2016.
*/
//Entidad para escribirle a la base de datos.
@Entity
class Noticia{


@Id
@GeneratedValue
private Long id;


private String tituloNoticia;

public Noticia() { //constructor vacío porque así lo dice JPA
}


public Noticia(String tituloNoticia) {
this.tituloNoticia = tituloNoticia;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTituloNoticia() {
return tituloNoticia;
}

public void setTituloNoticia(String tituloNoticia) {
this.tituloNoticia = tituloNoticia;
}

@Override
public String toString() {

//es buena práctica sobreescribir el método toString para poder conocer el contenido de cada objeto
return "Noticia{" +

"id=" + id +
", tituloNoticia='" + tituloNoticia + '\'' +
'}';
}
}

 

 

3. Interfaz que hable con la BD. Que escriba y lea las noticias en/de la BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Nov/2016.
*/
//Interfaz que hable con la base de datos y que escriba las noticias que quiero almacenar.
interface NoticiasRepositorio extends JpaRepository<Noticia, Long>{

//por convención hay que llamar los métodos en inglés y automáticamente SpringBoot intuye el nombre de la propiedad
// en este caso solo agrego el findBy=buscaPor
// TituloNoticia = nuestro objeto Noticia tiene una propiedad tituloNoticia
Collection<Noticia> findByTituloNoticia(String tn);


// Agrego un método para que busque por id
Noticia findById(Long id);


}

4. Servicio RESTful.

 

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Nov/2016.
*/
/* Para exponer un servicio REST, solo hay que agregar el controlador = Controller con la anotación @RestController */
@RestController
class NoticiasRestController{


/*Agregamos la información de la ruta para encontrar el servicio*/
@RequestMapping("/noticias")

Collection<Noticia> noticias(){
return noticiasRepositorio.findAll();
}

@Autowired
private NoticiasRepositorio noticiasRepositorio;

}



5. Aplicación principal que levanta el servicio.

 

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 09/Nov/2016.
*/
@SpringBootApplication
public class DemoSpringBootApplication {

/* Código para escribir en la base de datos algunas noticias.
- la clase principal debe tener la anotación @SpringBootApplication.
*/
private static final Logger log = LoggerFactory.getLogger(DemoSpringBootApplication.class);


public static void main(String[] args) {
SpringApplication.run(DemoSpringBootApplication.class, args);
}

/*
- Utilizamos la interfa CommandLineRunner que nos proporciona SpringBoot y
tiene un solo método que tiene la función de cargar cuando se ejecuta el main.
- Recuerda que nuestra base de datos es h2 para este ejemplo, pero puedes utilizar el manejador de BD que te guste.
- Escribimos el runner como un bean para que sea detectado por spring boot y que cargue lo que necesitamos
al arranque*/
@Bean
CommandLineRunner commandLineRunner(NoticiasRepositorio nr){

//como es java 8 puedo ocupar un lambda ya que esta interfaz solo tiene 1 método
return args -> {

//inserto en la BD
log.info("\n");

log.info("Guardando en la BD.\n");
Arrays.asList("Industria de Drones, mucho mas que cámaras voladoras.",
"Drones recuperarán autos robados en algunas zonas de la CDMX.",
"Drones para hackear al mundo",
"Japón quiere volver a ser el rey de las vías.",
"Pixar salvó a Steve Jobs y a Apple.")
.forEach( n -> nr.save(new Noticia(n)) );


log.info("Consulto todos los registros existentes en la BD.");
//busco los elementos y los imprimo en el log
nr.findAll().forEach(

n -> {log.info(n.toString());
});
log.info("\n");


log.info("Consulta por el método que agregamos.");
//ahora busco con el método que nosotros hicimos.
nr.findByTituloNoticia("Drones para hackear al mundo").forEach(

n -> {log.info("id de noticia encontrada por título: "+n.getId());
});

log.info("\n");

//Por último buscamos la noticia que tiene por id el número 2
Noticia n = nr.findById(2L);

log.info("Noticia encontrada por id ("+n.getId()+") título: "+n.getTituloNoticia());
log.info("\n");
};
};
}


6. Resultado.

 


1) La consulta en web de nuestro servicio RESTful.




2) En consola se muestran los registro que se consultaron de la BD y un par de métodos que agregamos.





Te puedes descargar mi código directamente del repositorio en github o te los puedes descargar en formato zip.


Artículos Relacionados



- Spring Boot INITIALIZR
- Spring Boot con Base de Datos

 

Como siempre dame un poquito de crédito y no le quites la firma a mi código.

Add a comment

Spring Boot - Servicio Data RESTful con acceso a datos con JPA (Front end basado en RESTful hypermedia)

- Prerequisitos

Genera tu proyecto con Spring Boot INITIALIZR.

- Spring boot starter test (este te lo agrega Spring por default para tus pruebas)

- Spring boot starter data JPA.

- H2 como manejador de BD (esta en particular vive en memoria y solo es para esta cápsula). Puedes utilizar la BD que prefieras.

- Spring boot starter data rest.

 

1. Configuración MAVEN

<groupId>com.ledzedev.springboot.datarest</groupId>
<artifactId>ledzedev-spring-boot-datarest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>ledzedev-spring-boot-datarest</name>
<description>Proyecto demo de un servicio de tipo Data Rest Spring Boot con Base de Datos</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

 

2. Empezamos con una entidad de usuarios y un enum que nos servirá para la propiedad de sexo (M=masculino y F=femenino)

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 11/Nov/2016.
*/
enum Sexo {

M, F
}


@Entity
class Usuario {


@Id
@GeneratedValue
private Long id;


private String nombre;

private Long edad;

private Sexo sexo;

public Usuario() {
}

public Usuario(String nombre, Long edad, Sexo sexo) {
this.nombre = nombre;
this.edad = edad;
this.sexo = sexo;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {
this.nombre = nombre;
}

public Long getEdad() {
return edad;
}

public void setEdad(Long edad) {
this.edad = edad;
}

public Sexo getSexo() {
return sexo;
}

public void setSexo(Sexo sexo) {
this.sexo = sexo;
}

@Override
public String toString() {

return "Usuario{" +
"id=" + id +
", nombre='" + nombre + '\'' +
", edad='" + edad + '\'' +
", sexo=" + sexo +
'}';
}
}

 

3. Agregamos la interfaz que habla con la BD.

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 11/Nov/2016.
*/
@RepositoryRestResource
interface UsuarioRepositorio extends JpaRepository<Usuario, Long> {

Collection<Usuario> findByNombre(@Param("nom") String nombre);
}

 

4. A la clase principal "LedzedevSpringBootDatarestApplication", le agregamos el CommandLineRunner para la carga inicial de nuestro servicio Rest.

- Agregamos 6 usuarios a la BD, como carga inicial y escribimos un log para indicar que la carga se realizó.


/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 11/Nov/2016.
*/
@Bean
CommandLineRunner runner(UsuarioRepositorio ur){
return x ->{
Arrays.asList(
new Usuario("Gerardo", 36L, Sexo.M) ,
new Usuario("Fidel", 33L, Sexo.M) ,
new Usuario("Hugo", 32L, Sexo.M) ,
new Usuario("Sarai", 27L, Sexo.F) ,
new Usuario("Anallely", 32L, Sexo.F) ,
new Usuario("Maribel", 35L, Sexo.F)
).forEach(ur::save);

log.info("Carga inicial en BD - Hecha.");
};
}

 

Resultados

- Corremos el main de nuestra clase LedzedevSpringBootDatarestApplication y como resultado debemos ver el texto del log que escribimos.

 

 

 

- Abrimos nuestro browser (Google Chrome en mi caso)

- http://localhost:8080/usuarios

El resultado nos debe mandar el json enriquecido (así lo llamo) de todas las propiedades que necesitamos.


 

NOTA: Podemos observar que a diferencia de un servicio RESTful simple, los servicios RESTful basados en hypermedia nos devuelven mas información valiosa y de hecho nos devuelve por defecto la paginación; lo único que tenemos que hacer es ver la definición original del servicio y ver como se puede mandar a llamar la paginación.

 

Tecleamos http://localhost:8080 y nos devuelve en automático la definición y el profile(que son las definiciones de todos los métodos)

El resultado de llamar con paginación. Por ejemplo:

Quiero la página 0.

Quiero que el tamaño de la página sea de 2 Usuarios (como tenemos 6 usuarios, tengo posibilidad de 3 páginas)

IMPORTANTE: el índice de las páginas empieza en cero, así que la 2a página sería la número 1, la 3a página, sería la número 2, etc. Ver la imagen a continuación.

Por default el servicio me devuelve los links para el manejo de la paginación (first, serlf, next, last).

 

- Por último probamos nuestro método para buscar por la propiedad nombre.

 

- Extra bonus

Agregar una propiedad al JSON que nos devuelve el servicio RESTful (basado en hypermedia)

Agregamos un ResourceProcessor y agregamos la propiedad que necesitamos:

/**
* Código generado por Gerado Pucheta Figueroa
* Twitter: @ledzedev
* http://ledze.mx
* 11/Nov/2016.
*/
@Component
class UsuarioResourceProcessor implements ResourceProcessor<Resource<Usuario>> {


@Override
public Resource<Usuario> process(Resource<Usuario> usuarioResource) {

usuarioResource.add(new Link("http://localhost:8080/images/usuario_"+usuarioResource.getContent().getId() + ".jpg","foto-perfil"));
return usuarioResource;
}
}

El resultado después de reiniciar nuestro servicio queda de la siguiente forma:

 

Te puedes descargar mi código directamente de mi repositorio en github o te los puedes descargar en formato zip.

 

Artículos Relacionados

- Spring Boot INITIALIZR
- Spring Boot - Servicio RESTful con Base de Datos

Documentación original

- Accessing JPA Data with REST / Accediendo a datos JPA con REST

 

 

 

 

 

Add a comment

Subcategories