Partial update REST in Spring Boot and Kotlin - spring

I have a project with Spring Boot + Kotlin + Morphia.
I need add partial update of my entities. My actual post method:
#PostMapping("update/")
fun updateStudent(#RequestBody #Valid student: Student, results: BindingResult): ResponseData<Student> {
if (results.hasErrors())
return ResponseData(errors = results.errors)
if (!student.canEdit(login.user))
return ResponseData()
student.save()
return ResponseData(data = student)
}
I need read student from database and update only the sended fields

This is my solution:
import org.springframework.beans.BeanWrapperImpl
import java.util.HashSet
fun getNullPropertyNames(source: Any): Array<String> {
val src = BeanWrapperImpl(source)
val pds = src.propertyDescriptors
val emptyNames = HashSet<String>()
for (pd in pds) {
if (src.getPropertyValue(pd.name) == null) emptyNames.add(pd.name)
}
return emptyNames.toTypedArray()
}
And in controller
import org.springframework.beans.BeanUtils
#RestController
class GateController {
#Autowired
private val modelRepository: MyRepository? = null
// allow both 'full' and 'partial' update
#PutMapping("/somemodel/{Id}")
fun updateModel(
#PathVariable Id: Long,
#RequestBody requestBody: SomeModel
): SomeModel {
var objFromDb = modelRepository!!.findById(Id).orElseThrow { ResourceNotFoundException("Object not found with id: " + Id) }
BeanUtils.copyProperties(requestBody, objFromDb, *getNullPropertyNames(requestBody))
return modelRepository.save(objFromDb)
}
...
}

There are 2 things to implement. Reading Student from DB and copying properties from the student from request.
I post java code but it's no problem to convert to kotlin
Morphia morphia = new Morphia();
db = new Mongo();
Datastore ds = morphia.createDatastore(db, appname, user, pass.toCharArray());
morphia.map(Student.class);
Student existing= ds.find(Student.class).field("id").equal(student.id).get();
Then you can use Apache BeanUtils
http://commons.apache.org/proper/commons-beanutils/javadocs/v1.8.3/apidocs/org/apache/commons/beanutils/BeanUtils.html#copyProperties%28java.lang.Object,%20java.lang.Object%29
BeanUtils.copyProperties(existing, student);
then
existing.save();

Related

Spring boot serialize kotlin enum by custom property

I have an Enum and I would like to serialize it using custom property. It works in my tests but not when I make request.
Enum should be mapped using JsonValue
enum class PlantProtectionSortColumn(
#get:JsonValue val propertyName: String,
) {
NAME("name"),
REGISTRATION_NUMBER("registrationNumber");
}
In test the lowercase case works as expected.
class PlantProtectionSortColumnTest : ServiceSpec() {
#Autowired
lateinit var mapper: ObjectMapper
data class PlantProtectionSortColumnWrapper(
val sort: PlantProtectionSortColumn,
)
init {
// this works
test("Deserialize PlantProtectionSortColumn enum with custom name ") {
val json = """
{
"sort": "registrationNumber"
}
"""
val result = mapper.readValue(json, PlantProtectionSortColumnWrapper::class.java)
result.sort shouldBe PlantProtectionSortColumn.REGISTRATION_NUMBER
}
// this one fails
test("Deserialize PlantProtectionSortColumn enum with enum name ") {
val json = """
{
"sort": "REGISTRATION_NUMBER"
}
"""
val result = mapper.readValue(json, PlantProtectionSortColumnWrapper::class.java)
result.sort shouldBe PlantProtectionSortColumn.REGISTRATION_NUMBER
}
}
}
But in controller, when i send request with lowercase I get 400. But when the request matches the enum name It works, but response is returned with lowercase. So Spring is not using the objectMapper only for request, in response it is used.
private const val RESOURCE_PATH = "$API_PATH/plant-protection"
#RestController
#RequestMapping(RESOURCE_PATH, produces = [MediaType.APPLICATION_JSON_VALUE])
class PlantProtectionController() {
#GetMapping("/test")
fun get(
#RequestParam sortColumn: PlantProtectionSortColumn,
) = sortColumn
}
I believe kqr's answer is correct and you need to configure converter, not JSON deserializer.
It could look like:
#Component
class StringToPlantProtectionSortColumnConverter : Converter<String, PlantProtectionSortColumn> {
override fun convert(source: String): PlantProtectionSortColumn {
return PlantProtectionSortColumn.values().firstOrNull { it.propertyName == source }
?: throw NotFoundException(PlantProtectionSortColumn::class, source)
}}
In your endpoint you are not parsing json body but query parameters, which are not in json format.

How to expose data to SSE from a #Tailable query in Spring Data Mongo

After reading the docs of #Tailable of Spring Data MongoDB, I think it is good to use it for message notifications.
#SpringBootApplication
class ServerApplication {
#Bean
fun runner(template: ReactiveMongoTemplate) = CommandLineRunner {
println("running CommandLineRunner...")
template.executeCommand("{\"convertToCapped\": \"messages\", size: 100000}");
}
fun main(args: Array<String>) {
runApplication<ServerApplication>(*args)
}
}
---------
#RestController()
#RequestMapping(value = ["messages"])
#CrossOrigin(origins = ["http://localhost:4200"])
class MessageController(private val messages: MessageRepository) {
#PostMapping
fun hello(p: String) =
this.messages.save(Message(body = p, sentAt = Instant.now())).log().then()
#GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun messageStream(): Flux<Message> = this.messages.getMessagesBy().log()
}
-----------
interface MessageRepository : ReactiveMongoRepository<Message, String> {
#Tailable
fun getMessagesBy(): Flux<Message>
}
------------
#Document(collection = "messages")
data class Message(#Id var id: String? = null, var body: String, var sentAt: Instant = Instant.now())
How to implement it?
Done it by myself, check my solution.
I have resolved this issue myself, check the sample codes.
Also, published a post on medium to demonstrate how to use it a SPA client written in Angular.

Is there anyway to update #Bean at runtime?

For my project I want to download from an API and store this information in a map. Furthermore I want to have the map as a bean in another class. I suspect the API to update regularly so I have set a #Schedule for downloading the XML file from the API.
To the problem... How can I update the map with the information from the API every time the XML is downloaded. I do not want to reboot the application each time.
I am very new to the Spring framework so if there is a more elegant method to do this please let me know.
data class DataContainer(val dictionary: MutableMap<String, String>)
#Configuration
#Component
class DownloadRenhold {
var dict: MutableMap<String, String> = xmlToDict("/renhold.xml")
val dataContainer: DataContainer
#Bean
get() = DataContainer(dict)
fun download(link: String, path: String) {
URL(link).openStream().use { input ->
FileOutputStream(File(path)).use { output ->
input.copyTo(output)
}
}
}
#Scheduled(fixedRate = 5000)
fun scheduledDL() {
download("www.link.com","src/main/resources/renhold.xml")
dict = xmlToDict("/renhold.xml")
}
class Controller {
#GetMapping(value = ["/{orgnummer}"]) // #RequestMapping(value="/",method=RequestMethod.GET)
fun orgNrRequest(#PathVariable("orgnummer") nr: String): String? {
var actx = AnnotationConfigApplicationContext(DownloadRenhold::class.java)
var dataContainer = actx.getBean(DataContainer::class.java)
return dataContainer.dictionary[nr]
}
```
I would suggest to not have DataContainer as a bean directly. Instead inject DownRenhold into Controller as a singleton bean. Something along these lines:
// No need to make this class a Configuration. Plain Component would suffice.
// #Configuration
#Component
class DownloadRenhold {
var _dataContainer: DataContainer = null
var dataContainer: DataContainer
get() = _dataContainer
#Scheduled(fixedRate = 5000)
fun scheduledDL() {
_dataContainer = // do your download thing and create a DataContainer instance.
}
}
class Controller {
#Autowired
var dataProvider: DownloadRenhold
#GetMapping(value = ["/{orgnummer}"])
#RequestMapping(value="/",method=RequestMethod.GET)
fun orgNrRequest(#PathVariable("orgnummer") nr: String): String? {
dataProvider.dataContainer // access the current data container
}

How to make the PUT method work? I want to update my data in Table

This is my POST method and it is successful and run well. My question is how to do the PUT request method so that it can update the data well?
Post method
public void addRecipe(RecipeDTO recipedto)
{
Category categoryTitle = categoryRepository.findByCategoryTitle(recipedto.getCategoryTitle());
Recipe recipe = new Recipe();
/*I map my dto data original model*/
recipe.setRID(recipedto.getrID());
recipe.setRecipeTitle(recipedto.getRecipeTitle());
recipe.setDescription(recipedto.getDescription());
recipe.setCookTime(recipedto.getCookTime());
List categoryList = new ArrayList<>();
categoryList.add(categoryTitle);
recipe.setCategories(categoryList);
Recipe savedRecipe = recipeRepository.save(recipe);
/*I map the data in ingredientDTO and setpDTO to actual model */
List ingredientList = new ArrayList<>();
for(IngredientDTO ingredientdto : recipedto.getIngredients())
{
Ingredient ingredient = new Ingredient();
ingredient.setIID(ingredientdto.getiID());
ingredient.setIngredientName(ingredientdto.getIngredientName());
ingredient.setRecipe(savedRecipe);
ingredientList.add(ingredient);
}
List stepList = new ArrayList<>();
for(StepDTO stepdto : recipedto.getSteps())
{
Step step = new Step();
step.setSID(stepdto.getsID());
step.setStepDescription(stepdto.getStepDescription());
step.setStepNumber(stepdto.getStepNumber());
step.setRecipe(savedRecipe);
stepList.add(step);
}
ingredientRepository.save(ingredientList);
stepRepository.save(stepList);
}
This is my put method and it wont work, how should I do it, because I have no idea. Please teach me to do this method, if it is better.
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(id==recipedto.getrID().toString())
{
recipeRepository.save(recipe);
}
}
When building REST services in Java you usually use an Framework to help you with this.
Like "jax-rs": https://mvnrepository.com/artifact/javax.ws.rs/javax.ws.rs-api/2.0
If using jax-rs then you mark your method as an Http PUT method with the annotation #PUT, ex:
#PUT
#Path("ex/foo")
public Response somePutMethod() {
return Response.ok().entity("Put some Foos!").build();
}
If using Spring as an Framework you mark your PUT method with the #RequestMapping annotation, ex:
#RequestMapping(value = "/ex/foo", method = PUT)
public String putFoos() {
return "Put some Foos";
}
Firstly and very importantly, you DO NOT use String == String for checking equality. Your code:
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(id==recipedto.getrID().toString())
{
recipeRepository.save(recipe);
}
}
It should be:
public void updateRecipe(RecipeDTO recipedto, String id)
{
Recipe recipe = recipeRepository.findByrID(recipedto.getrID());
if(recipedto.getrID().toString().equals(id))
{
recipeRepository.save(recipe);
}
}
Why?
Because equality with == checks if objects have the same memory address. In other words:
new Integer(1) == new Integer(1) //false
1 == 1 //true
new String("hello") == new String("hello") //false
"hello" == "hello" //true because literal strings are stored in a String pool
new String("hello") == "hello" //false
Secondly, you SHOULD ALWAYS use generics with Collection APIs.
Your code:
List categoryList = new ArrayList<>();
Should be:
List<Category> categoryList = new ArrayList<>();
And lastly, like askepan said, you have not defined what framework you are using. In case of Jersey (JAX-RS implementation) you have HTTP Request Methods:
#GET, #POST, #PUT, #DELETE, #HEAD, #OPTIONS.
#PUT
#Produces("text/plain")
#Consumes("text/plain")
public Response putContainer() {
System.out.println("PUT CONTAINER " + container);
URI uri = uriInfo.getAbsolutePath();
Container c = new Container(container, uri.toString());
Response r;
if (!MemoryStore.MS.hasContainer(c)) {
r = Response.created(uri).build();
} else {
r = Response.noContent().build();
}
MemoryStore.MS.createContainer(c);
return r;
}
If you use Spring, there are #RequestMapping(method = ), or short versions:
#GetMapping, #PutMapping, #PostMapping, #DeleteMapping.
#GetMapping("/{id}")
public Person getPerson(#PathVariable Long id) {
// ...
}
#PutMapping
public void add(#RequestBody Person person) {
// ...
}
According to the annotation, the method will be called accordingly.
More information in:
Spring,JAX-RS

How to make AuditorAware work with Spring Data Mongo Reactive

Spring Security 5 provides a ReactiveSecurityContextHolder to fetch the SecurityContext from a Reactive context, but when I want to implement AuditorAware and get audition work automatically, but it does not work. Currently I can not find a Reactive variant for AuditorAware.
#Bean
public AuditorAware<Username> auditor() {
return () -> ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.log()
.filter(a -> a != null && a.isAuthenticated())
.map(Authentication::getPrincipal)
.cast(UserDetails.class)
.map(auth -> new Username(auth.getName()))
.switchIfEmpty(Mono.empty())
.blockOptional();
}
I have added #EnableMongoAuduting on my boot Application class.
On the Mongo document class. I added audition related annotations.
#CreatedDate
private LocalDateTime createdDate;
#CreatedBy
private Username author;
When I added a post, the createdDate is filled, but author is null.
{"id":"5a49ccdb9222971f40a4ada1","title":"my first post","content":"content of my first post","createdDate":"2018-01-01T13:53:31.234","author":null}
The complete codes is here, based on Spring Boot 2.0.0.M7.
Update: Spring Boot 2.4.0-M2/Spring Data Common 2.4.0-M2/Spring Data Mongo 3.1.0-M2 includes a ReactiveAuditorAware, Check this new sample, Note: use #EnableReactiveMongoAuditing to activiate it.
I am posting another solution which counts with input id to support update operations:
#Component
#RequiredArgsConstructor
public class AuditCallback implements ReactiveBeforeConvertCallback<AuditableEntity> {
private final ReactiveMongoTemplate mongoTemplate;
private Mono<?> exists(Object id, Class<?> entityClass) {
if (id == null) {
return Mono.empty();
}
return mongoTemplate.findById(id, entityClass);
}
#Override
public Publisher<AuditableEntity> onBeforeConvert(AuditableEntity entity, String collection) {
var securityContext = ReactiveSecurityContextHolder.getContext();
return securityContext
.zipWith(exists(entity.getId(), entity.getClass()))
.map(tuple2 -> {
var auditableEntity = (AuditableEntity) tuple2.getT2();
auditableEntity.setLastModifiedBy(tuple2.getT1().getAuthentication().getName());
auditableEntity.setLastModifiedDate(Instant.now());
return auditableEntity;
})
.switchIfEmpty(Mono.zip(securityContext, Mono.just(entity))
.map(tuple2 -> {
var auditableEntity = (AuditableEntity) tuple2.getT2();
String principal = tuple2.getT1().getAuthentication().getName();
Instant now = Instant.now();
auditableEntity.setLastModifiedBy(principal);
auditableEntity.setCreatedBy(principal);
auditableEntity.setLastModifiedDate(now);
auditableEntity.setCreatedDate(now);
return auditableEntity;
}));
}
}
Deprecated: see the update solution in the original post
Before the official reactive AuditAware is provided, there is an alternative to implement these via Spring Data Mongo specific ReactiveBeforeConvertCallback.
Do not use #EnableMongoAuditing
Implement your own ReactiveBeforeConvertCallback, here I use a PersistentEntity interface for those entities that need to be audited.
public class PersistentEntityCallback implements ReactiveBeforeConvertCallback<PersistentEntity> {
#Override
public Publisher<PersistentEntity> onBeforeConvert(PersistentEntity entity, String collection) {
var user = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(it -> it != null && it.isAuthenticated())
.map(Authentication::getPrincipal)
.cast(UserDetails.class)
.map(userDetails -> new Username(userDetails.getUsername()))
.switchIfEmpty(Mono.empty());
var currentTime = LocalDateTime.now();
if (entity.getId() == null) {
entity.setCreatedDate(currentTime);
}
entity.setLastModifiedDate(currentTime);
return user
.map(u -> {
if (entity.getId() == null) {
entity.setCreatedBy(u);
}
entity.setLastModifiedBy(u);
return entity;
}
)
.defaultIfEmpty(entity);
}
}
Check the complete codes here.
To have createdBy attribute filled, you need to link your auditorAware bean with the annotation #EnableMongoAuditing
In your MongoConfig class, define your bean :
#Bean(name = "auditorAware")
public AuditorAware<String> auditor() {
....
}
and use it in the annotation :
#Configuration
#EnableMongoAuditing(auditorAwareRef="auditorAware")
class MongoConfig {
....
}

Resources