odrotbohm on main
#1822 - Add native runtime hint… (compare)
@ctruzzi Not without violating the HAL spec. In essence, you can write your own media type => https://docs.spring.io/spring-hateoas/docs/1.2.0/reference/html/#mediatypes.custom
You can also look at the community-provided media types as templates:
Hello what is the best practice wo handle in the links with combination of DDD with validation on it... If maybe make a abstraction again on REST entpoint or not... or if there a better way to create the _link
eg. when I use a valid object from a customer
. afford<CustomerResource> { methodOn(CustomerResource::class.java).add(customer = customer) }
val customer = Customer(id= null, firstName = FirstName("firstName"), lastName = LastName("lastName"))
@GetMapping(value = ["/customers/{id}"], produces = [MediaTypes.HAL_JSON_VALUE])
fun one(@PathVariable id: String): EntityModel<Customer> {
// TODO: 09.02.21 Validation problems
val customer = Customer(id= Any().toString(), firstName = FirstName(Any().toString()), lastName = LastName(Any().toString()))
val create: Affordance = afford<CustomerResource> { methodOn(CustomerResource::class.java).add(customer = customer) }
or should I create the Domain objects eg on the service
level... sound not nice for me... because I pass already the controller
maybe I miss understood something....
@odrotbohm will be nice update on yout spring-restbucks
;) how handle everything together..
@marzelwidmer I can understand having a DTO that targets the serialized JSON, and having it carry validation rules embedded in either the constructors or the setters. This is the format used to "present" your API. After validating things in the controller, I would then transform into the internal type used at the service level. And I would do that transformation in the controller. The service doesn't have to be aware of this API-level of things.
If the validation rules you have in mind apply at the service level, put your validation rules there. Otherwise, keep them in the DTO. Then, when you transform stuff in the controller, you can have suitable error handling to report to the user of the API what went wrong.
@gregturn @odrotbohm Thanks for the response, I try to explain again where I have a conceptional ("knopf-im-kopf") problem in my head. I try to follow (resp. impl) https://speakerdeck.com/olivergierke/domain-driven-design-and-rest-1?slide=15
Also use this classes to validate on the API side. When I have to use this classes then also to create my _links
I have to pass also valid (from my business valid objects) values (what is normal because of validation)
If I use the same (what 1st come in my head who should be correct) objects.
I was asking my self don't if it better to don't use the same object
on API side. because the java.lang.IllegalArgumentException
are throwing on runtime and If the developer not have the Domain
validation in mine when he create the HATEOS
resp. _links
with the methodOn(...
@gregturn when I get U right is it also valid to create the API validation from the service layer (after transformation)
data class Customer(val id: String? = UUID.randomUUID().toString(), val firstName: FirstName, val lastName: LastName)
// Domain Primitive
data class FirstName(val value: String) {
init {
require(value.isNotEmpty()) { "value must be non-empty" }
require(value.trim().length >= 2) { "wrong value length" }
require(value.trim().length <= 20) { "wrong value length" }
}
companion object {
@JvmStatic
@JsonCreator
fun create(value: String) = FirstName(value)
}
@JsonValue
override fun toString() = value
}
.....
@RestController
class CustomerResource(private val service: CustomerService) {
val CREATE_REL = "create"
val ALL_CUSTOMERS_REL = "customers"
@GetMapping(value = ["/customers/{id}"], produces = [MediaTypes.HAL_JSON_VALUE])
fun oneCustomer(@PathVariable id: String): EntityModel<Customer> {
val customer = Customer(
id = null, firstName = FirstName("Here-I-Have-To-Add-Some-Valid-Domain-Primitive-Value"),
lastName = LastName("TO-Avoid-IllegalArgumentException")
)
// java.lang.IllegalArgumentException: wrong value length
// at ch.keepcalm.demo.customer.secureByDesignModel.FirstName.<init>(Customer.kt:13)
val selfLink = linkTo(methodOn(CustomerResource::class.java).oneCustomer(id)).withSelfRel()
val createCustomerLink = linkTo(methodOn(CustomerResource::class.java).addCustomer(customer = customer)).withRel(CREATE_REL)
val allCustomersLink = linkTo(methodOn(CustomerResource::class.java).allCustomers()).withRel(ALL_CUSTOMERS_REL)
return EntityModel.of(service.findCustomerById(id))
.add(selfLink, createCustomerLink, allCustomersLink)
}
@GetMapping(value = ["/customers"], produces = [MediaTypes.HAL_JSON_VALUE])
fun allCustomers(): CollectionModel<EntityModel<Customer>> {
val selfLink: Link = linkTo(methodOn(CustomerResource::class.java).allCustomers()).withSelfRel()
return CollectionModel.of(
service.findCustomers().map {
EntityModel.of(it, linkTo(methodOn(CustomerResource::class.java).oneCustomer(it.id.toString())).withSelfRel())
}, selfLink
)
}
}
There's a couple of things to discuss here: I think, when manually implementing controllers, you should use dedicated DTOs to wrap into representation models and map from and to them explicitly. Using the domain types, especially inbound, is problematic as the strong rule enforcement will cause the deserialization break at the first rule violation. Unfortunately Jackson currently does not support accumulating the errors to return them all at once reasonably. Outbound, you still face the challenge of having to adapt the entity boundaries to the representation boundaries. Aggregates are a good start but as soon as you get away from the standard "CRUD via HTTP" the symmetry between the representation and a persisted entity usually goes off anyway. I.e. there will be resources that are centered around a DTO that then is used to map to different aggregates and invokes state transitions to them or actually needs different information assembled into a single view on the outbound side.
Does that help?
Hi, i am trying to use the Links class for a client-side implementation with a value that comes from the link header.
They return the value linkHeaderValue = 'https://url.com?page=1; rel=first, https://url.com?page=2; rel=last'
But when i call Links.parse(linkHeaderValue), i get an empty Links object.Links.parse("<https://url.com?page=1>; rel=\"first\", <https://url.com?page=1>; rel=\"last\"")
This is because the linkHeaderValue doesn't match the LINK_HEADER_PATTERN in the Links class.
If they would return 'https://url.com?page=1; rel="first", https://url.com?page=2; rel="last"' it would work.
The docs of the producer say that they implement this spec for the link header.
But i am not really certain if they produce data up to standard of the specs or Links class does.
So i have 2 questions:
ERROR!
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.hateoas.hal.Jackson2HalModule$HalLinkListSerializer': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.hateoas.hal.Jackson2HalModule$HalLinkListSerializer]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.hateoas.hal.Jackson2HalModule$HalLinkListSerializer.<init>()
any help ?
I'm trying to store an object that extends RepresentationModel
in Redis cache, upon retrieval I get a serialisation error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'org.springframework.hateoas.Links' as a subtype of java.util.List<org.springframework.hateoas.Link>: Not a subtype
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 507] (through reference chain: ...
Hi I am having Compile Issue using HalModelBuilder in Kotlin Code. I am on all the latest versions. But it comes down to a single method build() on HalModelBuilder
I have created as simple a test class. The compile error is Not enough information to infer type variable T
- a typing issue - Here is the example
it seems like this should be simple to solve. If you need me to I can raise an issue on the project. Sorry if this has been asked before.
import org.springframework.hateoas.mediatype.hal.HalModelBuilder
// My domain class which is does NOT extend RepresentationModel
data class MyDomainDobject(
val id: Int
)
fun main () {
// the domain object I want to convert
val myDomainDobject = MyDomainDobject(123)
// create a hal model builder, for the domain object
val builder = HalModelBuilder.halModelOf(myDomainDobject)
// from here I would add links, embeds etc......
// the following line causes a compile issue
// - Not enough information to infer type variable T
val model = builder.build()
// the build method (is generically typed, whereas HalModelBuilder is not.
// so cannot infer any type. Regardless the build method seems to assume it is returning
// a underlying domain object which extends RepresentationModel<T>, which it isn't in my case!
// my workaround comes in the form of a JAVA STATIC METHOD, which forces the type
// conversion for me. It works for now but must compile Java code in Kotlin project
// public static <T> EntityModel<T> build( HalModelBuilder builder ) {
// return (EntityModel<T>) builder.build();
// }
}
Hi everyone, I'm in the process of upgrading Spring Hateoas from 0.8x to the current version. It's quite a big application so I'm working through a large set of changes. There are a few places in our code where we are making use of the injectable _halObjectMapper
. It seems like the current version is reusing a copy of an object mapper I define: https://github.com/spring-projects/spring-hateoas/blob/1.2.x/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java#L49
My question is, is there any way to get a handle on the hal configured ObjectMapper via some kind of injectable class?
"I want to use the RepresentationModelAssemblerSupport to make the assembler aware of the resources controller to get the free self links, and for code organization purposes, but I'm using HAL and I also want to potentially embed objects on single resources.
It seems if I use the Hal builder I can no longer use that assembler because it's typed to go from T to D extends RepresentationModel 😕"
rest-message.properties
and normal message.properties
https://docs.spring.io/spring-hateoas/docs/current/reference/html/#mediatypes.hal.configuration the sampe about the HEATOAS problems is this used of the message.properties
when not I don't know how I can call the messges in my program code from a rest-message.properties
I am not sure if this related in this channel because of this I post the same message in the sprong-boot
ResponseEntity
subclasses any more :-(
I'm really happy to announce the next release of Spring HATEOAS Siren! Version 1.2.0 is available through Maven Central and based on Spring HATEOAS 1.3.1!
Source: https://github.com/ingogriebsch/spring-hateoas-siren
Documentation: https://ingogriebsch.github.io/spring-hateoas-siren/1.2.0/index.html
The examples project is also updated to reflect the latest changes. Feel free to check it out to understand how to use the features provided by the library.
https://github.com/ingogriebsch/spring-hateoas-siren-samples
Every feedback is very welcome! :)
Hi
It seems that Links have a problem with java 17 records since they are final.
java.lang.IllegalArgumentException: Cannot subclass final class com.example.demo.DemoApplication$Hello
at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:660) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy.generate(ClassLoaderAwareGeneratorStrategy.java:57) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:358) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:110) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:108) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54) ~[spring-core-5.3.10.jar:5.3.10]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:419) ~[spring-core-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:57) ~[spring-aop-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:206) ~[spring-aop-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-5.3.10.jar:5.3.10]
at org.springframework.hateoas.server.core.DummyInvocationUtils.getProxyWithInterceptor(DummyInvocationUtils.java:193) ~[spring-hateoas-1.4.0-M3.jar:1.4.0-M3]
at org.springframework.hateoas.server.core.DummyInvocationUtils.access$000(DummyInvocationUtils.java:38) ~[spring-hateoas-1.4.0-M3.jar:1.4.0-M3]
at org.springframework.hateoas.server.core.DummyInvocationUtils$InvocationRecordingMethodInterceptor.invoke(DummyInvocationUtils.java:90) ~[spring-hateoas-1.4.0-M3.jar:1.4.0-M3]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.10.jar:5.3.10]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.10.jar:5.3.10]
at com.example.demo.DemoApplication$DemoController$$EnhancerBySpringCGLIB$$19e5f7b7.hi(<generated>) ~[classes/:na]
or at least to control its definition? I'm not sure why the default is going to the "abandon" link in this:
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL_FORMS)
public class TransactionController {
@PostMapping("/sca/transactions")
EntityModel<?> createTransaction(@RequestBody TransactionCreate transaction) {
var model = TransactionSummary.builder()
.scaTransactionId(UUID.randomUUID())
.status("threeDSMethodNotificationPending")
.threeDSMethodData("base64-encoded-json-string")
.threeDSVersion("2.2.0")
.threeDSMethodURL("http://host/route")
.build();
var methodInvocation = methodOn(TransactionController.class).getTransaction(model.getScaTransactionId());
var link = Affordances.of(linkTo(methodInvocation).withSelfRel()).toLink();
model.add(link);
model.add(createLink("abandonTransaction", TransactionAbandon.class,
methodOn(TransactionController.class).abandonTransaction(model.getScaTransactionId(), null)));
model.add(createLink("threeDSMethodNotification", ThreeDSMethodNotification.class,
methodOn(TransactionController.class).threeDSMethodNotification(model.getScaTransactionId(), null)));
model.add(createLink("challengeResponseNotification", ChallengeResponseNotification.class,
methodOn(TransactionController.class).challengeResponseNotification(model.getScaTransactionId(), null)));
model.add(createLink("challengeResultsNotification", ChallengeResponseNotification.class,
methodOn(TransactionController.class).challengeResultsNotification(model.getScaTransactionId(), null))); // todo fix input type
return EntityModel.of(model);
}
produces this:
"scaTransactionId": "05da9337-13f1-4fe1-92a1-0cbc18b8384c",
"status": "threeDSMethodNotificationPending",
"threeDSVersion": "2.2.0",
"threeDSMethodURL": "http://host/route",
"threeDSMethodData": "base64-encoded-json-string",
"_links": {
"self": {
"href": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c"
},
"abandonTransaction": {
"href": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/abandon"
},
"threeDSMethodNotification": {
"href": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/threeDSMethodNotification"
},
"challengeResponseNotification": {
"href": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/challengeResponseNotification"
},
"challengeResultsNotification": {
"href": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/challengeResultsNotification"
}
},
"_templates": {
"challengeResultsNotification": {
"method": "post",
"properties": [
{
"name": "cres",
"readOnly": true,
"type": "text"
}
],
"target": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/challengeResultsNotification"
},
"default": {
"method": "post",
"properties": [
{
"name": "reason",
"readOnly": true,
"type": "text"
}
],
"target": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/abandon"
},
"abandonTransaction": {
"method": "post",
"properties": [
{
"name": "reason",
"readOnly": true,
"type": "text"
}
],
"target": "http://localhost:8080/sca/transactions/05da9337-13f1-4fe1-92a1-0cbc18b8384c/abandon"
}, etc etc etc