This problem make me physically ill.
Joke aside, I've been trying to add an authentication layer to my web app using spring-boot with security plugin. Here is my data class.
#Document(collection = "user")
data class User (
var name : String,
var password : String,
var email : String,
var type : String,
var status : String,
var balance : Int
){
#Id val id : String = ObjectId.get().toHexString()
}
After some searching, Ctr+C, Ctr+V, I'm successfully set-up some custom authentication that will get user information from database, look like this:
override fun loadUserByUsername(name : String): UserDetails {
logger.info(name)
val user = repo.findByName(name)
return User(user!!.name,passwordEncoder.encode(user.password),AuthorityUtils.NO_AUTHORITIES)
}
Here where the fun begin, its seem that the code never run pass val user = repo.findByName(name). Worst thing is, there are no exception being thrown, the code run to that line and the rest just disappear.
Out of frustration, I decide to fake the return object so that I can get pass the authentication like this:
override fun loadUserByUsername(name : String): UserDetails {
logger.info(name)
//val user = repo.findByName(name)
logger.debug("asdkfhasdklfjhasdf")
return User("string",passwordEncoder.encode("you"),AuthorityUtils.NO_AUTHORITIES)
}
Now, finally I can get some exception:
{
"timestamp": "2018-11-08T18:08:29.541+0000",
"status": 500,
"error": "Internal Server Error",
"message": "No accessor to set property #org.springframework.data.annotation.Id()private final java.lang.String com.sonnbh.jwt.User.id!",
"path": "/user"
}
The exception state that spring cannot access property id so I change the type of id from val to var.
#Document(collection = "user")
data class User (
var name : String,
var password : String,
var email : String,
var type : String,
var status : String,
var balance : Int
){
#Id var id : String = ObjectId.get().toHexString()
}
Finally, my app work as expected. However, after some attempt trying to dig deeper to the problem, I found that this problem only occur to spring-boot v2.1.0. My old project which use spring-boot v2.0.5 actually run fine with val id. This led me to some question:
Did I my old implement of data class User properly? I just want to prevent any change to User.id after its being read from database or init. What can I do to improve?
Why spring-boot v2.1 can't access to the property like spring-boot v2.0.5 did?
Spring Data in 2.1. has changed the way in which it deals with final fields in entities. It no longer uses reflection to override the immutability of the fields, which in general is good. There are a few ways to cope with the problem.
They are described here: https://jira.spring.io/browse/DATACMNS-1374?focusedCommentId=182289&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-182289
Here's what the Spring guys recommend:
Add a #PersistenceConstructor to construct the entity that sets immutable fields.
Add wither methods (MyEntity withXxx(…)) to create a new instance that contains the changed property value.
Alternatively: Use Kotlin's data class feature. This will basically do the same as wither methods.
Can only answer the first part; you could try moving the declaration of ID to be apart of the constructor? That will satisfy your requirement of only initialising when the object is created and it will still be read only.
Related
I'm using interface based projection to get certain fields from db. one of my field name start with is. I'm able to get the field from database via native query, however, the response returned by spring boot controller does not contain is in field name. How should I resolve it?
interface UserProjection {
val userId: Long
val isPrivate: Boolean
val likesCount: Int
}
Query
SELECT u.user_id as userId, u.is_private as private, u.likes_count as likesCount FROM users u WHERE u.user_id=?;
However, response returned by spring boot is
{
"userId": 12345,
"private": false,
"likesCount": 1
}
The solution is to use a fun to get the field instead of a val
interface LoginUserProjection {
val id: Long
val passwordHash: String
fun getIsVerifiedAccount(): Boolean
}
I have a moshi PolymorphicJsonAdapterFactory and it works great.
.withSubtype(ColdWeather::class.java, "Cold")
.withSubtype(HotWeather::class.java, "Hot")
.withDefaultValue(//how to grab the label)
The method withDefaultValue is a great catch all, but my BE team wants me to log the actual label that comes down in order to help catch a bug that's going on on their end. As far as I can tell... in the withDefaultValue I can't grab a reference to the label which in this case the backend is sending back "Medium".
I feel like there must be a way to grab this label (but I'm missing something simple?) so I can log it and possibly propagate it in the withDefaultValue method.
I stumbled on the issue a while ago. I found it impossible to achieve with just using .withDefaultValue method. So far I did not find better solution other than .withFallbackJsonAdapter (I am using moshi version 1.12), which lets you parse the json manually in case the label is unknown to your PolymorphicJsonAdapterFactory adapter. The documentation says:
/**
* Returns a new factory that with default to {#code fallbackJsonAdapter.fromJson(reader)} upon
* decoding of unrecognized labels.
*
* <p>The {#link JsonReader} instance will not be automatically consumed, so make sure to consume
* it within your implementation of {#link JsonAdapter#fromJson(JsonReader)}
*/
public PolymorphicJsonAdapterFactory<T> withFallbackJsonAdapter(
#Nullable JsonAdapter<Object> fallbackJsonAdapter) {
return ...
}
I assume your code is somewhat like this (simplified):
interface Weather {
val type: String
}
#JsonClass(generateAdapter = true)
class ColdWeather( #Json(name = "type") override val type: String) : Weather
#JsonClass(generateAdapter = true)
class HotWeather( #Json(name = "type") override val type: String) : Weather
val weatherAdapter = PolymorphicJsonAdapterFactory.of(Weather::class.java, "type")
.withSubtype(ColdWeather::class.java, "Cold")
.withSubtype(HotWeather::class.java, "Hot")
and you receive a json similar to this:
{
"weather" : {
"type" : "Cold"
}
}
To receive an unknown label, I would do something like this:
class UnknownWeather(override val type: String) : Weather
val weatherAdapter = PolymorphicJsonAdapterFactory.of(Weather::class.java, "type")
.withSubtype(ColdWeather::class.java, "Cold")
.withSubtype(HotWeather::class.java, "Hot")
.withFallbackJsonAdapter((object : JsonAdapter<Any>() {
override fun fromJson(reader: JsonReader): UnknownWeather {
var type = ... // parse it from the reader
return UnknownWeather(type)
}
override fun toJson(writer: JsonWriter, value: Any?) {
// nothing to do
}
}))
Of course that means that you will have to dig a bit into JsonReader, but it has a fairly understandable interface, you basically iterate through the properties of the json object and extract what you need, in our case just the "type" property.
FYI, seems like more people had problem with this: https://github.com/square/moshi/issues/784
The problem's pretty straightforward. I have a couple of events that derive from the same interface, and I'd like to deserialize them to their propper super-class.
I know how to do that with an object mapper, but using my own mapper would mean letting Spring-Boot parse the #RequestBody as a String and then doing it myself, which isn't the worlds end, but I can't help but suspect that Spring provides proper tools to handle this kind of situation. Trouble is, I can't seem to find them.
Here's a bit of sample code:
example event:
interface YellowOpsEvent {
val user: String
val partner: String
val subject: String
val change: NatureOfChange
}
data class StatusChangedEvent(override val user: String,
override val partner: String,
override val subject: String,
val before: String,
val after: String): YellowOpsEvent {
override val change = NatureOfChange.Changed
}
controller:
#PostMapping("/event")
fun writeEvent(#RequestBody event: YellowOpsEvent) { // < I expect this not to throw an exception
val bugme = event is StatusChangedEvent // < I expect this to return true if I send the proper event data.
}
Just to clarify, I perfectly understand why this doesn't work out of the box. The trouble is, I can't find out what I need to do to make it work.
The link in pL4Gu33's comment lead me in the right direction, but it took some additional searching and fiddling, plucking information from here and there to arrive at the solution that would finally work, so I'm summarising it here for completeness.
The trouble is that you'll need two annotations, one on the interface and one on the implementing classes, the combined use of which seems somewhat ill-documented.
First, on the interface, add this annotation. Contrary to some tutorials you will find, no further annotation of the interface is required:
#JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="#class")
interface YellowOpsEvent {
val user: String
val partner: String
val subject: String
val change: NatureOfChange
}
According to some documentation, this alone should be enough for propper deserialisation. The spring-boot controller, however, will throw an exception because the passed root name does not match the class it was expecting.
// the above will throw an exception when the serialization product is sent to this controller:
#PostMapping("/event")
fun writeEvent(#RequestBody event: YellowOpsEvent) { // < I expect this not to throw an exception
val bugme = event is StatusChangedEvent // < I expect this to return true if I send the proper event data.
}
To fix that, add the #JsonRootName annotation to any implementing classes, with the interface's name. Most documentation of this annotation don't use it for this, instead just for renaming the type, and even when it's mentioned in the linked question in the context of polymorphism, it wrongly uses its own name. This is what it needs to look like:
#JsonRootName("YellowOpsEvent")
data class StatusChangedEvent(override val user: String,
override val partner: String,
override val subject: String,
val before: String,
val after: String): YellowOpsEvent {
override val change = NatureOfChange.Changed
}
Now it works! :)
I'm importing historical football (or soccer, if you're from the US) data into a Neo4j database using a spring boot application (2.1.6.RELEASE) with the spring-boot-starter-data-neo4j dependency and a standalone, locally running 3.5.6 Neo4j database server.
But for some reason searching for an entity by a simple property and an attached, referenced entity, does not work, althought the relation is present in the database.
This is the part of the model, that is currently giving me a headache:
#NodeEntity(label = "Season")
open class Season(
#Id
#GeneratedValue
var id: Long? = null,
#Index(unique = true)
var name: String,
var seasonNumber: Long,
#Relationship(type = "IN_LEAGUE", direction = Relationship.OUTGOING)
var league: League?,
var start: LocalDate,
var end: LocalDate
)
#NodeEntity(label = "League")
open class League(
#Id
#GeneratedValue
var id: Long? = null,
#Index(unique = true)
var name: String,
#Relationship(type = "BELONGS_TO", direction = Relationship.OUTGOING)
var country: Country?
)
(I left out the Country class, as I'm pretty sure that it is not part of the problem)
To allow running the import more than once, I want to check if the corresponding entity is already present in the database and only import newer ones. So I added the following method SeasonRepository:
open class SeasonRepository : CrudRepository<Season, Long> {
fun findBySeasonNumberAndLeague(number: Long, league: League): Season?
}
But it is giving me a null result instead of the existing entity on consecutive runs, hence I get duplicates in my database.
I would have expected spring-data-neo4j to reduce the passed League to its Id and then have a generated query that looks somewhat like this:
MATCH (s:Season)-[:IN_LEAGUE]->(l:League) WHERE id(l) = {leagueId} AND s.seasonNumber = {seasonNumber} WITH s MATCH (s)-[r]->(o) RETURN s,r,o
but when I turn on finer logging on the neo4j package I see this output in the log file:
MATCH (n:`Season`) WHERE n.`seasonNumber` = { `seasonNumber_0` } AND n.`league` = { `league_1` } WITH n RETURN n,[ [ (n)-[r_i1:`IN_LEAGUE`]->(l1:`League`) | [ r_i1, l1 ] ] ], ID(n) with params {league_1={id=30228, name=1. Bundesliga, country={id=29773, name=Deutschland}}, seasonNumber_0=1}
So for some reason, spring-data seems to think, that the league property is a simple / primitive property and not a full releation, that needs to be resolved by the id (n.league= {league_1}).
I only got it to work, by passing the id of the league, and providing a custom query using the #Query annotation but I actually thought, that it would work with spring-data-neo4j out of the box.
Any help appreciated. Let me know if you need more details.
Spring Data Neo4j does not support objects as parameters at the moment. It is possible to query for properties on related entities/nodes e.g. findBySeasonNumberAndLeagueName if this is a suitable solution.
Preface
I want to create a sub-resource of another resource in one call. These resources have a #ManyToMany relationship: Users and Groups.
I do not want to create first a user, then the group and after that the relation as it is shown in Working with Relationships in Spring Data REST - simply because I think a resource that cannot exist on its own, such as a group, should only be created if at least one user is also associated with that resource. For this I require a single endpoint like this one (which is not working for me, otherwise I wouldn't be here) that creates a group and also sets the associated "seeding" user in one transaction.
Currently, the only way to make this work for me is to "synchronize" the relation manually like this:
public void setUsers(Set<AppUser> users) {
users.forEach(u -> u.getGroups().add(this));
this.users = users;
}
this would allow me to
POST http://localhost:8080/groups
{
"name": "Group X",
"users": ["http://localhost:8080/users/1"]
}
but my problem with that is that this does not feel right to me - it does seem like a workaround and not the actual Spring-way to make this requirement work. So ..
I'm currently struggling with creating relational resources using Spring's #RepositoryRestResource. I want to create a new group and associate it with the calling user like this:
POST http://localhost:8080/users/1/groups
{
"name": "Group X"
}
but the only result is the response 204 No Content. I have no idea why. This may or may not be related to another question of mine (see here) where I try to achieve the same by setting the relating resource in the JSON payload - that doesn't work either.
Server side I am getting the following error:
tion$ResourceSupportHttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class org.springframework.hateoas.Resources<java.lang.Object>]]: java.lang.NullPointerException
Please let me know in case you need any specific code.
Tried
I added exported = false to the #RepositoryRestResource of UserGroupRepository:
#RepositoryRestResource(collectionResourceRel = "groups", path = "groups", exported = false)
public interface UserGroupRepository extends JpaRepository<UserGroup, Long> {
List<UserGroup> findByName(#Param("name") String name);
}
and I am sending:
PATCH http://localhost:8080/users/1
{
"groups": [
{
"name": "Group X"
}
]
}
However, the result is still just 204 No Content and a ResourceNotFoundException on the server side.
Unit Test
Essentially, the following unit test is supposed to work but I can also live with an answer why this cannot work and which also shows how this is done correctly.
#Autowired
private TestRestTemplate template;
private static String USERS_ENDPOINT = "http://localhost:8080/users/";
private static String GROUPS_ENDPOINT = "http://localhost:8080/groups/";
// ..
#Test
#DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public void whenCreateUserGroup() {
// Creates a user
whenCreateAppUser();
ResponseEntity<AppUser> appUserResponse = template.getForEntity(USERS_ENDPOINT + "1/", AppUser.class);
AppUser appUser = appUserResponse.getBody();
UserGroup userGroup = new UserGroup();
userGroup.setName("Test Group");
userGroup.setUsers(Collections.singleton(appUser));
template.postForEntity(GROUPS_ENDPOINT, userGroup, UserGroup.class);
ResponseEntity<UserGroup> userGroupResponse = template.getForEntity(GROUPS_ENDPOINT + "2/", UserGroup.class);
Predicate<String> username = other -> appUser.getUsername().equals(other);
assertNotNull("Response must not be null.", userGroupResponse.getBody());
assertTrue("User was not associated with the group he created.",
userGroupResponse.getBody().getUsers().stream()
.map(AppUser::getUsername).anyMatch(username));
}
However, the line
userGroup.setUsers(Collections.singleton(appUser));
will break this test and return a 404 Bad Request.
According to SDR reference:
POST
Only supported for collection associations. Adds a new element to the collection. Supported media types:
text/uri-list - URIs pointing to the resource to add to the association.
So to add group to user try to do this:
POST http://localhost:8080/users/1/groups (with Content-Type:text/uri-list)
http://localhost:8080/groups/1
Additional info.