Skip to content

Commit 9c20517

Browse files
committed
Producto Mínimo Viable (MVP)
1 parent 3135043 commit 9c20517

8 files changed

+166
-83
lines changed

README.md

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Crea tu API (Datos Deportivos API)
2-
Este repositorio sirve para controlar el código del curso **["Crea tu API"](https://hijosdelspectrum.blogspot.com/p/spring-framework-crear-una-api.html)**
2+
Este repositorio sirve para realizar el código del curso **["Crea tu API"](https://hijosdelspectrum.blogspot.com/p/spring-framework-crear-una-api.html)**
33

44
## Contenido
55
Se generó un proyecto [gradle](https://gradle.org/) usando [Spring Initialzr](https://start.spring.io/) con la siguiente configuración:
@@ -46,12 +46,35 @@ Los puntos que se van a ver son:
4646
1. [`@OneToMany`](https://www.hijosdelspectrum.com/2020/03/orm-por-xml-con-relaciones-onetomany.html)
4747
1. [Data REST](https://www.hijosdelspectrum.com/2020/03/la-potencia-de-spring-data-rest.html) (endpoints HATEOAS)
4848
1. Personalizar payload con Jackson
49-
1. ORM por XML(II)
50-
1. [Herencia con varias subclases](https://www.hijosdelspectrum.com/2020/03/orm-por-xml-guardar-subclases-en.html)
51-
1. Callbacks y Listeners en JPA
52-
1. `@PostLoad` para inyectar un bean
53-
1. Deserializado de payload con `@JsonComponent`
54-
1. Inyectando bean con `JsonDeserializer`
49+
1. ORM por XML (II)
50+
1. Herencia con varias subclases ([SINGLE TABLE](https://www.hijosdelspectrum.com/2020/03/orm-por-xml-guardar-subclases-en.html))
51+
1. Inyectar bean en objetos no gestionados
52+
1. En [entidades leídas desde BD](https://www.hijosdelspectrum.com/2020/04/inyectar-un-bean-una-entidad-leida.html) (`Events` y `Listeners` en JPA)
53+
1. En objetos [desde las peticiones HTTP](https://www.hijosdelspectrum.com/2020/04/inyectar-un-bean-un-restresource.html) (`@JsonComponent`)
54+
1. Añadir código personalizado
55+
1. [Personalizar endpoints con `@RestResource`](https://www.hijosdelspectrum.com/2020/04/personalizar-endpoints-con-restresource.html)
56+
1. Añadir [método personalizado](https://www.hijosdelspectrum.com/2020/04/anadir-metodo-personalizado-en.html) a repositorio
57+
1. [Exponer método con `@RepositoryRestController`](https://www.hijosdelspectrum.com/2020/04/exponer-metodo-con-repositoryrestcontro.html)
58+
1. Añadir [link a endpoint `/search`](https://www.hijosdelspectrum.com/2020/05/anadir-link-resourcessearch.html)
59+
1. [Ruta con `@PathVariable`](https://www.hijosdelspectrum.com/2020/05/rutas-con-pathvariable.html)
60+
1. [Detección automática de links](https://www.hijosdelspectrum.com/2020/05/codigo-util-clase-configuracionrest.html) con `ConfiguracionRest`
61+
62+
63+
## Despliegue en local
64+
65+
Antes de arrancar la API se debe arrancar la BD. Se usa [H2](https://h2database.com/html/main.html) en modo servidor.
66+
Se puede levantar la BD usando el archivo `h2-version.jar`. Comprobar que tenemos acceso a la consola de H2 y que está corriendo.
67+
68+
Las propiedades de conexión son las que vienen por defecto:
69+
> url=jdbc:h2:tcp://localhost/~/test
70+
username=sa
71+
(sin password)
72+
73+
Entonces ejecutar la API con la última [release](https://github.com/LanyuEStudio/Datos-Deportivos-API/releases).
74+
75+
Mejor desde una consola para ver el log `java -jar datosdeportivosapi-VERSION.jar`.
76+
77+
Para conseguir este `.jar` lo mejor es [seguir los pasos indicados en el blog](https://www.hijosdelspectrum.com/2020/05/empaquetar-la-api-en-un-ficherojar.html).
5578

5679
Puedes utilizar esta colección Postman para probarla
5780

build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = 'es.lanyu'
8-
version = '0.0.1-SNAPSHOT'
8+
version = '1.0.0'
99
sourceCompatibility = '1.8'
1010

1111
repositories {
@@ -19,7 +19,7 @@ dependencies {
1919
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
2020
runtimeOnly 'com.h2database:h2'
2121
runtimeOnly 'org.postgresql:postgresql'
22-
implementation project(':datos-deportivos')
22+
implementation 'com.github.LanyuEStudio:datos-deportivos:v1.0.0'
2323
testImplementation('org.springframework.boot:spring-boot-starter-test') {
2424
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
2525
}

src/main/java/es/lanyu/datosdeportivosapi/ConfiguracionPorJava.java

-44
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
package es.lanyu.datosdeportivosapi;
22

3-
import java.lang.reflect.Method;
4-
import java.net.URI;
5-
import java.net.URISyntaxException;
6-
73
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
84
import org.springframework.context.annotation.Bean;
95
import org.springframework.context.annotation.ComponentScan;
106
import org.springframework.context.annotation.Configuration;
117
import org.springframework.context.annotation.PropertySource;
12-
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
13-
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
14-
import org.springframework.data.rest.webmvc.RepositorySearchesResource;
15-
import org.springframework.hateoas.Link;
16-
import org.springframework.hateoas.server.RepresentationModelProcessor;
178
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
189

1910
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
@@ -28,50 +19,15 @@
2819
import es.lanyu.comun.evento.Partido;
2920
import es.lanyu.comun.suceso.Gol;
3021
import es.lanyu.comun.suceso.Suceso;
31-
import es.lanyu.eventos.repositorios.PartidoConId;
3222
import es.lanyu.eventos.repositorios.SucesoConId;
3323
import es.lanyu.eventos.rest.MixIns;
34-
import es.lanyu.eventos.rest.PartidoController;
3524
import es.lanyu.participante.Participante;
3625
import es.lanyu.participante.repositorios.ParticipanteDAO;
3726

3827
@Configuration
3928
@PropertySource({ "classpath:config/rest.properties", "classpath:config/jackson.properties" })
4029
@ComponentScan("es.lanyu.eventos")
4130
public class ConfiguracionPorJava {
42-
43-
@Bean
44-
// Enlace con un par de formas (version antigua) de poner links en /search: https://stackoverflow.com/a/36007306
45-
RepresentationModelProcessor<RepositorySearchesResource> searchLinks(RepositoryRestConfiguration config) {
46-
return new RepresentationModelProcessor<RepositorySearchesResource>() {
47-
48-
@Override
49-
public RepositorySearchesResource process(RepositorySearchesResource searchResource) {
50-
// Esto se ejecuta para cualquier /search con lo que hay que filtrar cuando se usa
51-
if (searchResource.getDomainType().equals(PartidoConId.class)) {
52-
try {
53-
String nombreMetodo = "getPartidosConParticipanteComo";
54-
// https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/reference/html/#fundamentals.obtaining-links.builder.methods
55-
Method method = PartidoController.class.getMethod(nombreMetodo, String.class,
56-
PersistentEntityResourceAssembler.class);
57-
URI uri = org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
58-
.linkTo(method, null, null).toUri();
59-
// Lamentablemente no coge el basePath y hay que implementar esa parte
60-
// Sacado de: https://github.com/spring-projects/spring-hateoas-examples/tree/master/ //
61-
// spring-hateoas-and-spring-data-rest#altering-what-spring-data-rest-is-serving
62-
String url = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
63-
config.getBasePath() + uri.getPath(), uri.getQuery(), uri.getFragment()).toString();
64-
searchResource.add(new Link(url + "{?txt}", nombreMetodo));
65-
} catch (NoSuchMethodException | URISyntaxException e) {
66-
e.printStackTrace();
67-
}
68-
}
69-
70-
return searchResource;
71-
}
72-
73-
};
74-
}
7531

7632
@Bean
7733
public ServicioEntidad getServicioEntidad(ParticipanteDAO participanteDAO){

src/main/java/es/lanyu/datosdeportivosapi/DatosdeportivosapiApplication.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ public static void main(String[] args) {
4444
// participantes.stream().map(Participante::toString).forEach(log::trace);
4545

4646
// Listo los partidos en consola
47-
PartidoDAO partidoDAO = context.getBean(PartidoDAO.class);
47+
// PartidoDAO partidoDAO = context.getBean(PartidoDAO.class);
4848
// partidoDAO.findAll().stream().map(PartidoConId::toString).forEach(log::trace);
4949

50-
partidoDAO.getEventosConParticipanteConTexto("m").stream().map(PartidoConId::toString).forEach(log::trace);
50+
// partidoDAO.getEventosConParticipanteConTexto("m").stream().map(PartidoConId::toString).forEach(log::trace);
5151
// Ya no cierro la aplicacion
5252
// context.close();
5353
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package es.lanyu.eventos.rest;
2+
3+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
4+
5+
import java.lang.reflect.Method;
6+
import java.lang.reflect.Parameter;
7+
import java.net.URI;
8+
import java.net.URISyntaxException;
9+
import java.util.Arrays;
10+
import java.util.Collections;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.Stream;
15+
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
19+
import org.springframework.data.rest.webmvc.RepositorySearchesResource;
20+
import org.springframework.hateoas.Link;
21+
import org.springframework.hateoas.server.RepresentationModelProcessor;
22+
import org.springframework.web.bind.annotation.PathVariable;
23+
import org.springframework.web.bind.annotation.RequestParam;
24+
import org.springframework.web.bind.annotation.ResponseBody;
25+
import org.springframework.web.cors.CorsConfiguration;
26+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
27+
import org.springframework.web.filter.CorsFilter;
28+
29+
import es.lanyu.eventos.repositorios.PartidoConId;
30+
import es.lanyu.eventos.repositorios.SucesoConId;
31+
32+
/**
33+
* Configuracion de uso generalizado para distintos proyectos Spring Data Rest.
34+
* Proporciona las siguientes funcionalidades:
35+
* <ol>
36+
* <li>{@link #addSearchLinks(RepositoryRestConfiguration)}: enlaza cada
37+
* {@code /recursos/search} automaticamente con los metodos de los controladores registrados.</li>
38+
* <li>{@link #corsFilter()}: permite cualquier solicitud Cross-Origin.</li>
39+
* </ol>
40+
*
41+
* @author <a href="https://github.com/Awes0meM4n">Awes0meM4n</a>
42+
* @version 1.0
43+
* @since 1.0
44+
*/
45+
@Configuration
46+
public class ConfiguracionRest {
47+
48+
/**Enlaza automaticamente los links de los controladores registrados siguiendo las
49+
* <a href="https://www.hijosdelspectrum.com/2020/05/codigo-util-clase-configuracionrest.html">instrucciones </a>
50+
* @param config {@link RepositoryRestConfiguration} para recuperar al {@code basePath}
51+
* @return el bean del tipo {@code RepresentationModelProcessor<RepositorySearchesResource>}
52+
*/
53+
@Bean
54+
RepresentationModelProcessor<RepositorySearchesResource> addSearchLinks(RepositoryRestConfiguration config) {
55+
Map<Class<?>, Class<?>> controllersRegistrados = new HashMap<>();
56+
controllersRegistrados.put(PartidoConId.class, PartidoController.class);
57+
controllersRegistrados.put(SucesoConId.class, SucesoController.class);
58+
59+
return new RepresentationModelProcessor<RepositorySearchesResource>() {
60+
61+
@Override
62+
public RepositorySearchesResource process(RepositorySearchesResource searchResource) {
63+
if (controllersRegistrados.containsKey(searchResource.getDomainType())) {
64+
Method[] metodos = controllersRegistrados.get(searchResource.getDomainType()).getDeclaredMethods();
65+
for (Method m : metodos) {
66+
if (!m.isAnnotationPresent(ResponseBody.class)) continue;
67+
try {
68+
Object[] pathVars = Stream.of(m.getParameters())
69+
.filter(p -> p.isAnnotationPresent(PathVariable.class))
70+
.map(p -> "(" + p.getName() + ")")
71+
.collect(Collectors.toList()).toArray();
72+
URI uri = linkTo(m, pathVars).toUri();
73+
String path = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
74+
config.getBasePath() + uri.getPath(), uri.getQuery(), uri.getFragment())
75+
.toString().replace("(", "{").replace(")", "}");
76+
String requestParams = Stream.of(m.getParameters())
77+
.filter(p -> p.isAnnotationPresent(RequestParam.class))
78+
.map(Parameter::getName)
79+
.collect(Collectors.joining(","));
80+
searchResource.add(new Link(path + "{?" + requestParams + "}", m.getName()));
81+
} catch (URISyntaxException e) { e.printStackTrace(); }
82+
}
83+
}
84+
85+
return searchResource;
86+
}
87+
88+
};
89+
}
90+
91+
/**
92+
* Ver tambien <a href=
93+
* "https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.configuring-cors">
94+
* Configuring CORS</a> para configuracion Data Rest adicional heredada con
95+
* {@link org.springframework.web.bind.annotation.CrossOrigin}.
96+
*
97+
* @return bean del tipo {@link CorsFilter} permitiendo cualquier solicitud
98+
*/
99+
@Bean
100+
CorsFilter corsFilter() {
101+
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
102+
final CorsConfiguration config = new CorsConfiguration();
103+
config.setAllowCredentials(true);
104+
config.setAllowedOrigins(Collections.singletonList("*"));
105+
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
106+
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
107+
source.registerCorsConfiguration("/**", config);
108+
109+
return new CorsFilter(source);
110+
}
111+
112+
}

src/main/java/es/lanyu/eventos/rest/PartidoController.java

+17-25
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,32 @@
77
import org.springframework.data.rest.webmvc.RepositoryRestController;
88
import org.springframework.hateoas.CollectionModel;
99
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
1011
import org.springframework.web.bind.annotation.RequestParam;
1112
import org.springframework.web.bind.annotation.ResponseBody;
1213

1314
import es.lanyu.eventos.repositorios.PartidoConId;
1415
import es.lanyu.eventos.repositorios.PartidoDAO;
1516

1617
// Si se quiere inyectar PersistentEntityResourceAssembler hace falta
17-
// marcar el controlador con @RepositoryRestController
18-
// https://stackoverflow.com/a/51487736
19-
// Siguiendo la guia de la documentacion:
20-
// https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.overriding-sdr-response-handlers
18+
// marcar el controlador con @RepositoryRestController
2119
@RepositoryRestController
22-
// Se puede marcar todo el tipo como respuesta web
23-
// @ResponseBody
20+
@RequestMapping(path="/partidos/search")
2421
public class PartidoController {
25-
26-
private PartidoDAO partidoDAO;
27-
28-
PartidoController(PartidoDAO partidoDAO) {
22+
23+
private PartidoDAO partidoDAO;
24+
25+
PartidoController(PartidoDAO partidoDAO) {
2926
this.partidoDAO = partidoDAO;
3027
}
31-
32-
@GetMapping("/partidos/search/con-nombre-participante")
33-
// Para devolver una coleccion hay que usar CollectionModel<PersistentEntityResource>
34-
// Diferencia entre un recurso individual y una coleccion
35-
// https://docs.spring.io/spring-hateoas/docs/current/reference/html/#fundamentals.resources
36-
@ResponseBody
37-
public CollectionModel<PersistentEntityResource> getPartidosConParticipanteComo(@RequestParam String txt,
38-
// Inyectar PersistentEntityResourceAssembler para devolver como HATEOAS en el metodo
39-
// https://stackoverflow.com/a/31782016
40-
PersistentEntityResourceAssembler assembler) {
41-
List<PartidoConId> partidos = partidoDAO.getEventosConParticipanteConTexto(txt);
42-
43-
return assembler.toCollectionModel(partidos);
44-
}
45-
28+
29+
@GetMapping("/con-nombre-participante")
30+
@ResponseBody
31+
public CollectionModel<PersistentEntityResource> getPartidosConParticipanteComo(@RequestParam String txt,
32+
PersistentEntityResourceAssembler assembler) {
33+
List<PartidoConId> partidos = partidoDAO.getEventosConParticipanteConTexto(txt);
34+
35+
return assembler.toCollectionModel(partidos);
36+
}
37+
4638
}

src/main/resources/application.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ es.lanyu.jpa-package=${es.lanyu.entities-package}
1212

1313
spring.application.name=datos-deportivos-api
1414

15-
logging.level.es.lanyu.datosdeportivosapi.DatosdeportivosapiApplication=TRACE
15+
logging.level.es.lanyu=TRACE

src/main/resources/logback-spring.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#environment-properties -->
1414
<springProperty scope="context" name="profile" source="spring.profiles.active" defaultValue="default"/>
1515

16-
<property file="src/main/resources/application.properties" />
16+
<property resource="application.properties" />
1717
<property name="nombreApp" value="${spring.application.name}" />
1818

1919
<property name="lanyu.formato-fecha" value="%date{ddMMM HH:mm:ss, UTC}Z" />
@@ -22,7 +22,7 @@
2222

2323
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
2424
<encoder>
25-
<charset>UTF-8</charset>
25+
<charset>UTF-8</charset><!-- UTF-8 windows-1252 -->
2626
<pattern>${lanyu.formato-log}</pattern>
2727
</encoder>
2828
</appender>

0 commit comments

Comments
 (0)