Spring Boot - MongoDB - Inheritance - spring

I'm running into something odd with inheritance and mongodbrepositories.
I have the following:
`
#Document
public class Base {
public String fieldA;
}
public class Derived extends Base {
public String fieldB;
}
public interface DerivedRepository extends MongoRepository<Base, String> {
List<Derived> findByFieldA(String fieldA);
}
`
When inserting i get
Inserting DBObject containing fields: [_class, _id, fieldA, fieldB ]
in collection: base
When i do findByFieldA('some value') on the repository i get the following:
find using query: { "fieldA" : "some value" } fields: null for class:
class Derived in collection: derived
Any idea what is going on here? And how can I fix this, either by saving it to the proper derived collection or by querying from the base collection.
Regards,

First, I would make Derived class as document since the parent is going to be shared among many implementations.
public class Base {
public String fieldA;
}
#Document
public class Derived extends Base {
public String fieldB;
#Override
public String toString() {
return "{fieldA: " + getFieldA() + ", fieldB: " + fieldB + "}";
}
}
Second, change the repository specification with the type of document (class marked as #Document) as:
public interface DerivedRepository extends MongoRepository<Derived, String> {
List<Derived> findByFieldA(String fieldA);
List<Derived> findByFieldB(String fieldB);
}
I added extra method findByFieldB(String fieldB) to explain more.
With these changes, you should be able to query either with fieldA or fieldB as below:
public class SpringBootMongoApplication {
#Autowired
private DerivedRepository derivedRepository;
public void testMethod() throws Exception {
Derived derived1 = new Derived();
derived1.setFieldB("fieldB1");
derived1.setFieldA("fieldA1");
Derived derived2 = new Derived();
derived2.setFieldB("fieldB2");
derived2.setFieldA("fieldA2");
this.derivedRepository.save(Arrays.asList(derived1, derived2));
List<Derived> deriveds = this.derivedRepository.findByFieldA("fieldA1");
System.out.println(deriveds);
List<Derived> deriveds1 = this.derivedRepository.findByFieldB("fieldB2");
System.out.println(deriveds1);
}
}
The output should be:
[{fieldA: fieldA1, fieldB: fieldB1}]
[{fieldA: fieldA2, fieldB: fieldB2}]
You can also verify the object persisted and their types with mongo query as below:
I have created an Spring Boot sample app which you can find in Github.

Related

Spring data elasticsearch embedded field mapping

I'm struggling with mappings some field. I looked for an answer but couldn't find anything solving my case. Let's cut to the chase.
I have my document class
#Doucment
public class DocumentClass {
#Field(type = FieldType.Nested)
private EmployeeId employeeId;
}
An EmployeeId is wrapper for my uuid identifier. This object has nothing but just getters and setters and jackson annotations. The thing is that object extends some base class so such objects like EmployeeId can inherit this object. This super class has field id and this causes the problem. When I post some data to elasticsearch then it looks like this:
{
"employeeId": {
"id": "someUUID"
}
}
But I want to map this to be like:
{
"employeeId": "someUUID"
}
I wonder if there is a way to flatten this object.
If I get it right, you want to convert your EmployeeId class to a String and back. You have 2 possibilities to do that:
Using a property converter
If you only want to convert an EmployeeId in this entity and might keep it as it is in another, you should use a property converter that is only registered for this property:
import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter;
public class EmployeeIdConverter implements PropertyValueConverter {
#Override
public Object write(Object value) {
return value instanceof EmployeeId employeeId ? employeeId.getId() : value.toString();
}
#Override
public Object read(Object value) {
return new EmployeeId(value.toString());
}
}
This converter must be registered on the property, notice that the field type is set to Keyword as it probably should not be analysed:
import org.springframework.data.elasticsearch.annotations.ValueConverter;
#Document
public class DocumentClass {
#Field(type = FieldType.Keyword)
#ValueConverter(EmployeeIdConverter.class)
private EmployeeId employeeId;
}
Using a global converter
If you are using this EmployeeId at several places you might want register globally 2 converters for the two conversion directions:
#WritingConverter
public class EmployeeIdToString implements Converter<EmployeeId, String>{
#Nullable
#Override
public String convert(EmployeeId employeeId) {
return employeeId.getId();
}
}
#ReadingConverter
public class StringToEmployeeId implements Converter<String, EmployeeId>{
#Nullable
#Override
public EmployeeId convert(String id) {
return new EmployeeId(id);
}
}
To register these, you need to provide a custom client configuration (see the documentation):
#Configuration
public class MyClientConfig extends ElasticsearchConfiguration {
#Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
}
#Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new EmployeeIdToString());
converters.add(new StringToEmployeeId());
return new ElasticsearchCustomConversions(converters);
}
}
In this case, only the field type needs to be adjusted
#Document
public class DocumentClass {
#Field(type = FieldType.Keyword)
private EmployeeId employeeId;
}
I am guessing you have an Id field which is a string.
You need to put a #JsonValue annotation on that field to make jackson serialize it the way you want.
Field annotated by JsonValue will be used to serialize your pojo into json.
If your Id field is private, then add the annotation on the getter of that field.
https://fasterxml.github.io/jackson-annotations/javadoc/2.8/com/fasterxml/jackson/annotation/JsonValue.html

How to achieve row level authorization in spring-boot?

Assuming I've the following endpoints in spring boot
GET /todo
DELETE /todo/{id}
How can ensure that only entries for the userid are returned and that the user can only update his own todos?
I've a populated Authentication object.
Is there any build in way I can use? Or just make sure to always call findXyzByIdAndUserId where userid is always retrieved from the Principal?
I'm a bit worried about the possibility to forget the check and displaying entries from other users.
My approach to this would be a 3 way implementation: (using jpa & hibernate)
a user request context
a mapped superclass to get your context
a statement inspector to inject your userid
For example:
public final class UserRequestContext {
public static String getUserId() {
// code to retrieve your userid and throw when there is none!
if (userId == null) throw new IllegalStateException("userid null");
return userId;
}
}
#MappedSuperclass
public class UserResolver {
public static final String USER_RESOLVER = "USER_RESOLVER";
#Access(AccessType.PROPERTY)
public String getUserId() {
return UserRequestContext.getUserId();
}
}
#Component
public class UserInspector implements StatementInspector {
#Override
public String inspect(String statement) {
if (statement.contains(UserResolver.USER_RESOLVER)) {
statement = statement.replace(UserResolver.USER_RESOLVER, "userId = '" + UserRequestContext.getUserId() + "'" );
}
return sql;
}
#Bean
public HibernatePropertyCustomizer hibernatePropertyCustomizer() {
return hibernateProperies -> hibernateProperties.put("hibernate.session_factory.statement_inspector",
UserInspector.class.getName());
}
}
So your Entity looks like this:
#Entity
...
#Where(clause = UserResolver.USER_RESOLVER)
public class Todo extends UserResolver {
...
}

Get all documents from an index using spring-data-elasticsearch

I am trying to connect to my external ElasticSearch server with Spring Boot.
If I do a curl from command line, I get expected results.
curl "http://ipAddr:9200/indexName/TYPE/_search?pretty=true"
But getting this error when I try to access it via Spring Boot.
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Mon Sep 11 12:39:15 IST 2017</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div><div>Could not write JSON: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl["facets"])</div></body></html>
Not sure why a NullPointerException and what is aggregartion.impl
Here is my Spring Application:
Controller:
#RestController
public class PojoController {
#Autowired
PojoService pojoService;
#RequestMapping(value = "/", method=RequestMethod.GET)
public #ResponseBody String index() {
return new String("Welcome:)");
}
#RequestMapping(value = "/all", method = RequestMethod.GET,
produces = { MediaType.APPLICATION_JSON_VALUE })
#ResponseBody List<POJO> findAll() {
try {
List<POJO> pojoObj = pojoService.findAll();
return pojoObj;
} catch (Exception exp) {
exp.printStackTrace();
return null;
}
}
}
Repository:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
Service:
#Service
public class POJOServiceImpl implements POJOService{
private POJORepository pojoRepository;
private ElasticsearchTemplate elasticsearchTemplate;
#Autowired
public void setPojoRepository(PojoRepository pojoRepository) {
this.pojoRepository = pojoRepository;
}
public POJO findOne(String id) {
return pojoRepository.findOne(id);
}
public List<POJO> findAll() {
return (List<POJO>) pojoRepository.findAll();
}
}
POJO class:
#Document(indexName = "INDEX", type = "TYPE")
public class POJO {
#Id
private Integer id;
private String name;
public POJO(){
// empty
}
public POJO(Integerid, String name) {
super();
this.id = id;
this.name = name;
}
// getters and setters
}
I should be able to query all the documents in the index. Later on, I will try and use filters etc.
Any help is appreciated. Thanks :)
It looks like Jackson has a problem with handling your POJO (probably related to this issue: DATAES-274) - the problematic part is casting in repository from Iterable collection to List.
Update
In case of repositories, spring-data-elasticsearch behaves a bit different than you would expect. Taking your example:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
and after calling in your rest controller:
List<POJO> pojoObj = pojoService.findAll();
in debugger you will see something like this:
You would expect that pojoObj list contains objects of POJO class.
And here comes the surprise - pojoObj ArrayList contains one object of AggregatedPageImpl type and its content field is the right list that contains your POJO objects.
This is the reason why you get:
Could not write JSON: ... java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl[\"facets\"])
As I wrote before, Jackson cannot handle this while serializing POJO objects.
Solution 1
Let repositories return Iterable collection (by default).
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
}
Move the conversion part to the service but use some utility method (here with Guava) in order to have it like this:
import com.google.common.collect.Lists;
public List<POJO> findAll() {
return Lists.newArrayList(pojoRepository.findAll());
}
Solution 2
Use Page in repository (here simplified version without parameters):
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
Page<TestDto> findAll();
}
If you still want to operate on list - get content from page in service:
public List<POJO> findAll() {
return testDtoRepository.findAll().getContent();
}

Spring Data Rest Repository with abstract class / inheritance

I can't get Spring Data Rest with class inheritance working.
I'd like to have a single JSON Endpoint which handles all my concrete classes.
Repo:
public interface AbstractFooRepo extends KeyValueRepository<AbstractFoo, String> {}
Abstract class:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = MyFoo.class, name = "MY_FOO")
})
public abstract class AbstractFoo {
#Id public String id;
public String type;
}
Concrete class:
public class MyFoo extends AbstractFoo { }
Now when calling POST /abstractFoos with {"type":"MY_FOO"}, it tells me: java.lang.IllegalArgumentException: PersistentEntity must not be null!.
This seems to happen, because Spring doesn't know about MyFoo.
Is there some way to tell Spring Data REST about MyFoo without creating a Repository and a REST Endpoint for it?
(I'm using Spring Boot 1.5.1 and Spring Data REST 2.6.0)
EDIT:
Application.java:
#SpringBootApplication
#EnableMapRepositories
public class Application {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
I'm using Spring Boot 1.5.1 and Spring Data Release Ingalls.
KeyValueRepository doesn't work with inheritance. It uses the class name of every saved object to find the corresponding key-value-store. E.g. save(new Foo()) will place the saved object within the Foo collection. And abstractFoosRepo.findAll() will look within the AbstractFoo collection and won't find any Foo object.
Here's the working code using MongoRepository:
Application.java
Default Spring Boot Application Starter.
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AbstractFoo.java
I've tested include = JsonTypeInfo.As.EXISTING_PROPERTY and include = JsonTypeInfo.As.PROPERTY. Both seem to work fine!
It's even possible to register the Jackson SubTypes with a custom JacksonModule.
IMPORTANT: #RestResource(path="abstractFoos") is highly recommended. Else the _links.self links will point to /foos and /bars instead of /abstractFoos.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Foo.class, name = "MY_FOO"),
#JsonSubTypes.Type(value = Bar.class, name = "MY_Bar")
})
#Document(collection="foo_collection")
#RestResource(path="abstractFoos")
public abstract class AbstractFoo {
#Id public String id;
public abstract String getType();
}
AbstractFooRepo.java
Nothing special here
public interface AbstractFooRepo extends MongoRepository<AbstractFoo, String> { }
Foo.java & Bar.java
#Persistent
public class Foo extends AbstractFoo {
#Override
public String getType() {
return "MY_FOO";
}
}
#Persistent
public class Bar extends AbstractFoo {
#Override
public String getType() {
return "MY_BAR";
}
}
FooRelProvider.java
Without this part, the output of the objects would be separated in two arrays under _embedded.foos and _embedded.bars.
The supports method ensures that for all classes which extend AbstractFoo, the objects will be placed within _embedded.abstractFoos.
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class FooRelProvider extends EvoInflectorRelProvider {
#Override
public String getCollectionResourceRelFor(final Class<?> type) {
return super.getCollectionResourceRelFor(AbstractFoo.class);
}
#Override
public String getItemResourceRelFor(final Class<?> type) {
return super.getItemResourceRelFor(AbstractFoo.class);
}
#Override
public boolean supports(final Class<?> delimiter) {
return AbstractFoo.class.isAssignableFrom(delimiter);
}
}
EDIT
Added #Persistent to Foo.java and Bar.java. (Adding it to AbstractFoo.java doesn't work). Without this annotation I got NullPointerExceptions when trying to use JSR 303 Validation Annotations within inherited classes.
Example code to reproduce the error:
public class A {
#Id public String id;
#Valid public B b;
// #JsonTypeInfo + #JsonSubTypes
public static abstract class B {
#NotNull public String s;
}
// #Persistent <- Needed!
public static class B1 extends B { }
}
Please see the discussion in this resolved jira task for details of what is currently supported in spring-data-rest regarding JsonTypeInfo. And this jira task on what is still missing.
To summarize - only #JsonTypeInfo with include=JsonTypeInfo.As.EXISTING_PROPERTY is working for serialization and deserialization currently.
Also, you need spring-data-rest 2.5.3 (Hopper SR3) or later to get this limited support.
Please see my sample application - https://github.com/mduesterhoeft/spring-data-rest-entity-inheritance/tree/fixed-hopper-sr3-snapshot
With include=JsonTypeInfo.As.EXISTING_PROPERTY the type information is extracted from a regular property. An example helps getting the point of this way of adding type information:
The abstract class:
#Entity #Inheritance(strategy= SINGLE_TABLE)
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
include=JsonTypeInfo.As.EXISTING_PROPERTY,
property="type")
#JsonSubTypes({
#Type(name="DECIMAL", value=DecimalValue.class),
#Type(name="STRING", value=StringValue.class)})
public abstract class Value {
#Id #GeneratedValue(strategy = IDENTITY)
#Getter
private Long id;
public abstract String getType();
}
And the subclass:
#Entity #DiscriminatorValue("D")
#Getter #Setter
public class DecimalValue extends Value {
#Column(name = "DECIMAL_VALUE")
private BigDecimal value;
public String getType() {
return "DECIMAL";
}
}

Why it seems impossible to use BeanUtils.copyProperties from a JPA entity to a JAX-B Bean?

We are using JPA Entities to get the database rows and then when we transfer that to the external, we want to use disconnected object (DTO) which are simple beans annotated with JAX-B.
We use a mapper and its code looks like this:
public BillDTO map(BillEntity source, BillDTO target) {
BeanUtils.copyProperties(source, target);
return target;
}
But when the code is running we get an error like this:
java.lang.IllegalArgumentException: argument type mismatch
Note this is the Spring implementation of the BeanUtils:
import org.springframework.beans.BeanUtils
And the naming of the properties are identical (with their getter/setter).
Anybody knows why the error happens?
And how to use a fast way instead just copying properties one by one?
This example working well. Here String property is copied to enum property:
Entity:
public class A {
private String valueFrom;
public String getValue() {
return valueFrom;
}
public void setValue(String value) {
this.valueFrom = value;
}
}
DTO (En is enumeration):
public class B {
private En valueTo;
public void setValue(String def) {
this.valueTo = En.valueOf(def);
}
public void setEnumValue(En enumVal) {
this.valueTo = enumVal;
}
}
As for your GitHub example, problem in class B in getter should be:
public String getValue()
Example:
public String getValue() {
return value.toString();
}

Resources