java - resourcesupport - Resolución de URI de entidad en controlador personalizado(Spring HATEOAS)
org springframework hateoas resourcesupport (6)
Esto es más una nota al margen que una respuesta real, pero hace un tiempo logré copiar y pegar una clase para resolver las entidades desde una URL usando los métodos utilizados en SDR (solo más crudos). Probablemente hay una manera mucho mejor, pero hasta entonces, quizás esto ayude ...
@Service
public class EntityConverter {
@Autowired
private MappingContext<?, ?> mappingContext;
@Autowired
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryRestConfigurer> configurers = Collections.emptyList();
public <T> T convert(Link link, Class<T> target) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
PersistentEntities entities = new PersistentEntities(Arrays.asList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, conversionService);
conversionService.addConverter(converter);
addFormatters(conversionService);
for (RepositoryRestConfigurer configurer : configurers) {
configurer.configureConversionService(conversionService);
}
URI uri = convert(link);
T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target)));
if (object == null) {
throw new IllegalArgumentException(String.format("%s ''%s'' was not found.", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref());
} catch (Exception e) {
throw new IllegalArgumentException("URI from link is invalid", e);
}
}
private void addFormatters(FormatterRegistry registry) {
registry.addFormatter(DistanceFormatter.INSTANCE);
registry.addFormatter(PointFormatter.INSTANCE);
if (!(registry instanceof FormattingConversionService)) {
return;
}
FormattingConversionService conversionService = (FormattingConversionService) registry;
DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
conversionService);
converter.setApplicationContext(applicationContext);
}
}
Y sí, es probable que partes de esta clase sean simplemente inútiles. En mi defensa, fue solo un pequeño truco y nunca logré realmente necesitarlo, porque primero encontré otros problemas ;-)
Tengo un proyecto basado en Spring-Data-Rest y también tiene algunos puntos finales personalizados.
Para enviar datos POST estoy usando json como
{
"action": "REMOVE",
"customer": "http://localhost:8080/api/rest/customers/7"
}
Eso está bien para Spring-Data-Rest, pero no funciona con un controlador personalizado.
por ejemplo:
public class Action {
public ActionType action;
public Customer customer;
}
@RestController
public class ActionController(){
@Autowired
private ActionService actionService;
@RestController
public class ActionController {
@Autowired
private ActionService actionService;
@RequestMapping(value = "/customer/action", method = RequestMethod.POST)
public ResponseEntity<ActionResult> doAction(@RequestBody Action action){
ActionType actionType = action.action;
Customer customer = action.customer;//<------There is a problem
ActionResult result = actionService.doCustomerAction(actionType, customer);
return ResponseEntity.ok(result);
}
}
Cuando llamo
curl -v -X POST -H "Content-Type: application/json" -d ''{"action": "REMOVE","customer": "http://localhost:8080/api/rest/customers/7"}'' http://localhost:8080/customer/action
Tengo una respuesta
{
"timestamp" : "2016-05-12T11:55:41.237+0000",
"status" : 400,
"error" : "Bad Request",
"exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
"message" : "Could not read document: Can not instantiate value of type [simple type, class model.user.Customer] from String value (''http://localhost:8080/api/rest/customers/7''); no single-String constructor/factory method/n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[/"customer/"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class logic.model.user.Customer] from String value (''http://localhost:8080/api/rest/customers/7''); no single-String constructor/factory method/n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[/"customer/"])",
"path" : "/customer/action"
* Closing connection 0
}
Debido a que Spring Case no puede convertir un URI a una entidad de Cliente.
¿Hay alguna forma de utilizar el mecanismo de recuperación de datos de Spring para resolver entidades mediante sus URI?
Solo tengo una idea: usar JsonDeserializer personalizado con URI de análisis para extraer el ID de entidad y realizar una solicitud a un repositorio. Pero esta estrategia no me ayuda si tengo un URI como " http://localhost:8080/api/rest/customers/8/product " en ese caso no tengo valor de product.Id .
He estado teniendo el mismo problema durante mucho tiempo y lo resolví de la siguiente manera. @Florian estaba en el camino correcto y gracias a su sugerencia, encontré una manera de hacer que la conversión funcionara automáticamente. Hay varias piezas necesarias:
- Un servicio de conversión para habilitar la conversión de un URI a una entidad (aprovechando el UriToEntityConverter proporcionado con el marco)
- Un deserializador para detectar cuándo es apropiado invocar el convertidor (no queremos alterar el comportamiento predeterminado de SDR)
- Un módulo Jackson personalizado para empujar todo a SDR
Para el punto 1, la implementación se puede limitar a lo siguiente
import org.springframework.context.ApplicationContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.format.support.DefaultFormattingConversionService;
public class UriToEntityConversionService extends DefaultFormattingConversionService {
private UriToEntityConverter converter;
public UriToEntityConversionService(ApplicationContext applicationContext, PersistentEntities entities) {
new DomainClassConverter<>(this).setApplicationContext(applicationContext);
converter = new UriToEntityConverter(entities, this);
addConverter(converter);
}
public UriToEntityConverter getConverter() {
return converter;
}
}
Para el punto 2 esta es mi solución.
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import your.domain.RootEntity; // <-- replace this with the import of the root class (or marker interface) of your domain
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.util.Assert;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
public class RootEntityFromUriDeserializer extends BeanDeserializerModifier {
private final UriToEntityConverter converter;
private final PersistentEntities repositories;
public RootEntityFromUriDeserializer(PersistentEntities repositories, UriToEntityConverter converter) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
this.repositories = repositories;
this.converter = converter;
}
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(beanDesc.getBeanClass());
boolean deserializingARootEntity = entity != null && RootEntity.class.isAssignableFrom(entity.getType());
if (deserializingARootEntity) {
replaceValueInstantiator(builder, entity);
}
return builder;
}
private void replaceValueInstantiator(BeanDeserializerBuilder builder, PersistentEntity<?, ?> entity) {
ValueInstantiator currentValueInstantiator = builder.getValueInstantiator();
if (currentValueInstantiator instanceof StdValueInstantiator) {
EntityFromUriInstantiator entityFromUriInstantiator =
new EntityFromUriInstantiator((StdValueInstantiator) currentValueInstantiator, entity.getType(), converter);
builder.setValueInstantiator(entityFromUriInstantiator);
}
}
private class EntityFromUriInstantiator extends StdValueInstantiator {
private final Class entityType;
private final UriToEntityConverter converter;
private EntityFromUriInstantiator(StdValueInstantiator src, Class entityType, UriToEntityConverter converter) {
super(src);
this.entityType = entityType;
this.converter = converter;
}
@Override
public Object createFromString(DeserializationContext ctxt, String value) throws IOException {
URI uri;
try {
uri = new URI(value);
} catch (URISyntaxException e) {
return super.createFromString(ctxt, value);
}
return converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(entityType));
}
}
}
Luego para el punto 3, en el RepositoryRestConfigurerAdapter personalizado,
public class MyRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(new SimpleModule("URIDeserializationModule"){
@Override
public void setupModule(SetupContext context) {
UriToEntityConverter converter = conversionService.getConverter();
RootEntityFromUriDeserializer rootEntityFromUriDeserializer = new RootEntityFromUriDeserializer(persistentEntities, converter);
context.addBeanDeserializerModifier(rootEntityFromUriDeserializer);
}
});
}
}
Esto funciona sin problemas para mí y no interfiere con ninguna conversión desde el marco (tenemos muchos puntos finales personalizados). En el punto 2, la intención era habilitar la creación de instancias desde un URI solo en los casos donde:
- La entidad que se está deserializando es una entidad raíz (no hay propiedades)
- La cadena provista es un URI real (de lo contrario, vuelve al comportamiento predeterminado)
Llegué a la siguiente solución. Es un poco hackish, pero funciona.
Primero, el servicio para convertir los URI en entidades.
EntityConverter
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.geo.format.DistanceFormatter;
import org.springframework.data.geo.format.PointFormatter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
@Service
public class EntityConverter {
@Autowired
private MappingContext<?, ?> mappingContext;
@Autowired
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryRestConfigurer> configurers = Collections.emptyList();
public <T> T convert(Link link, Class<T> target) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
Repositories repositories = new Repositories(applicationContext);
UriToEntityConverter converter = new UriToEntityConverter(
new PersistentEntities(Collections.singleton(mappingContext)),
new DefaultRepositoryInvokerFactory(repositories),
repositories);
conversionService.addConverter(converter);
addFormatters(conversionService);
for (RepositoryRestConfigurer configurer : configurers) {
configurer.configureConversionService(conversionService);
}
URI uri = convert(link);
T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target)));
if (object == null) {
throw new IllegalArgumentException(String.format("registerNotFound", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref().replace("{?projection}", ""));
} catch (Exception e) {
throw new IllegalArgumentException("invalidURI", e);
}
}
private void addFormatters(FormatterRegistry registry) {
registry.addFormatter(DistanceFormatter.INSTANCE);
registry.addFormatter(PointFormatter.INSTANCE);
if (!(registry instanceof FormattingConversionService)) {
return;
}
FormattingConversionService conversionService = (FormattingConversionService) registry;
DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
conversionService);
converter.setApplicationContext(applicationContext);
}
}
Segundo, un componente para poder usar el EntityConverter
fuera del contexto de Spring.
AplicaciónContextHolder
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
}
Tercero, constructor de entidad que toma otra entidad como entrada.
MyEntity
public MyEntity(MyEntity entity) {
property1 = entity.property1;
property2 = entity.property2;
property3 = entity.property3;
// ...
}
Cuarto, el constructor de entidades que toma una String
como entrada, que debería ser el URI.
MyEntity
public MyEntity(String URI) {
this(ApplicationContextHolder.getContext().getBean(EntityConverter.class).convert(new Link(URI.replace("{?projection}", "")), MyEntity.class));
}
Opcionalmente, he movido parte del código anterior a una clase de Utils
.
Llegué a esta solución mirando el mensaje de error del post de la pregunta, que también recibía. Spring no sabe cómo construir un objeto a partir de una String
? Lo mostraré cómo ...
Sin embargo, como se dice en un comentario, no funciona para los URI de entidades anidadas.
Mi solución será algo compacta. No estoy seguro de que pueda ser útil para todos los casos, pero para una relación simple como .../entity/{id}
podría analizar. Lo he probado en SDR & Spring Boot 2.0.3.RELEASE
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Collections;
@Service
public class UriToEntityConversionService {
@Autowired
private MappingContext<?, ?> mappingContext; // OOTB
@Autowired
private RepositoryInvokerFactory invokerFactory; // OOTB
@Autowired
private Repositories repositories; // OOTB
public <T> T convert(Link link, Class<T> target) {
PersistentEntities entities = new PersistentEntities(Collections.singletonList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, invokerFactory, repositories);
URI uri = convert(link);
Object o = converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(target));
T object = target.cast(o);
if (object == null) {
throw new IllegalArgumentException(String.format("%s ''%s'' was not found.", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref());
} catch (Exception e) {
throw new IllegalArgumentException("URI from link is invalid", e);
}
}
}
Uso:
@Component
public class CategoryConverter implements Converter<CategoryForm, Category> {
private UriToEntityConversionService conversionService;
@Autowired
public CategoryConverter(UriToEntityConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Category convert(CategoryForm source) {
Category category = new Category();
category.setId(source.getId());
category.setName(source.getName());
category.setOptions(source.getOptions());
if (source.getParent() != null) {
Category parent = conversionService.convert(new Link(source.getParent()), Category.class);
category.setParent(parent);
}
return category;
}
}
Solicite a JSON como:
{
...
"parent": "http://localhost:8080/categories/{id}",
...
}
No puedo creerlo. Después de envolver mi cabeza alrededor de esto durante el MES (!) ¡ Logré RESOLVER ESTO !
Algunas palabras de introducción:
Spring HATEOAS utiliza URI como referencias a entidades. Y proporciona un gran soporte para obtener estos enlaces URI para una entidad determinada . Por ejemplo, cuando un cliente solicita una entidad que hace referencia a otras entidades secundarias, el cliente recibirá esos URI. Agradable trabajar con
GET /users/1
{
"username": "foobar",
"_links": {
"self": {
"href": "http://localhost:8080/user/1" //<<<== HATEOAS Link
}
}
}
El cliente REST solo trabaja con esos uris. El cliente REST NO DEBE conocer la estructura de estos URI. El cliente REST no sabe que hay un ID interno de la base de datos al final de la cadena URI.
Hasta ahora tan bueno. PERO los datos de primavera HATEOAS no ofrece ninguna funcionalidad para convertir un URI de nuevo a la entidad correspondiente (cargada desde la base de datos). Todo el mundo necesita eso en controladores REST personalizados. (Ver pregunta arriba)
Piense en un ejemplo en el que desee trabajar con un usuario en un controlador REST personalizado. El cliente enviaría esta solicitud.
POST /checkAdress
{
user: "/users/1"
someMoreOtherParams: "...",
[...]
}
¿Cómo debe deserializarse el controlador REST personalizado del uri (String) al UserModel? Encontré una forma: debe configurar la deserialización de Jackson en su RepositoryRestConfigurer:
RepositoryRestConfigurer.java
public class RepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Autowired
UserRepo userRepo;
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.addDeserializer(UserModel.class, new JsonDeserializer<UserModel>() {
@Override
public UserModel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String uri = p.getValueAsString();
//extract ID from URI, with regular expression (1)
Pattern regex = Pattern.compile(".*///" + entityName + "///(//d+)");
Matcher matcher = regex.matcher(uri);
if (!matcher.matches()) throw new RuntimeException("This does not seem to be an URI for an ''"+entityName+"'': "+uri);
String userId = matcher.group(1);
UserModel user = userRepo.findById(userId)
.orElseThrow(() -> new RuntimeException("User with id "+userId+" does not exist."))
return user;
}
});
objectMapper.registerModule(module);
}
}
(1) Este análisis de cadena es feo. Lo sé. Pero es solo el inverso de org.springframework.hateoas.EntityLinks y sus implementaciones. Y el autor de spring-hateos se niega obstinadamente a ofrecer métodos de utilidad para ambas direcciones.
Para HAL con @RequestBody
use el Resource<T>
como parámetro del método en lugar de entidad Action
para permitir la conversión de URI de recursos relacionados
public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){