how not to consider #NotBlank in some methods - spring-boot

I'm doing a restful app in Spring boot,jpa,mysql. I have annoted some of my model fields #NotBlank to print an error in the creation of an object if those fields are blank.
Now when i'm updating, I don't want to get that error if I don't set some fields in my json body.My goal is to update just the fields which are present.
So I want to know if there is a way not to consider an #NotBlank in my updating method.
This is the code source :
For the Entity
public class Note implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank(name)
private String title;
#NotBlank
private String content;
//Getters and Setters
}
The controller
#RestController
#RequestMapping("/api")
public class NoteController {
#Autowired
NoteRepository noteRepository;
// Create a new Note
#PostMapping("/notes")
public Note createNote(#Valid #RequestBody Note note) {
return noteRepository.save(note);
}
// Update a Note
#PutMapping("/notes/{id}")
public Note partialUpdateNote(#PathVariable(value = "id") Long noteId,
#RequestBody Note noteDetails) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
//copyNonNullProperties(noteDetails, note);
if(note.getTitle()!= null) {
note.setTitle(noteDetails.getTitle());
}else {
note.setTitle(note.getTitle());
}
if(note.getContent()!= null) {
note.setContent(noteDetails.getContent());
}else {
note.setContent(note.getContent());
}
Note updatedNote = noteRepository.save(note);
return updatedNote;
}
// Delete a Note
#DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(#PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
noteRepository.delete(note);
return ResponseEntity.ok().build();
}
}
ResourceNotFoundException is the class responsible to throws errors.

You can use groups for that.
Add two interfaces CreateGroup and UpdateGroup.
Use them by this way:
#NotBlank(groups = CreateGroup.class)
#Null(groups = UpdateGroup.class)
private String title;
In the create endpoint
#Valid #ConvertGroup(from = Default.class, to = CreateGroup.class) Note note
In the update endpoint
#Valid #ConvertGroup(from = Default.class, to = UpdateGroup.class) Note note
Probably you don't need UpdateGroup. It is just to show a common approach.
Also for the nested objects inside Note something like
#ConvertGroup(from = CreateGroup.class, to = UpdateGroup.class)
can be used.

Related

Spring Boot JPA update after fething ID is not working

#Autowired
LessonService lsnService;
#PutMapping(path = "/{id}")
public ResponseEntity<Object> updateLesson(#PathVariable("id") Long id, #Valid #RequestBody LessonDto dto) {
try {
lsnService.findById(id);
dto.setId(id);
lsnService.save(dto);
return ResponseEntity.ok(dto);
}
catch (Exception e) {
ApiErrorMessage errorMessage = new ApiErrorMessage();
errorMessage.setStatusCode(400L);
errorMessage.setMessage(e.getMessage());
errorMessage.setDescription("The server cannot or will not process the request due to an apparent client error");
return ResponseEntity.badRequest().body(errorMessage);
}
}
Here's my problem. When I remove lsnService.findById(id);, update is working.
If I didn't add that code if a user update with unexisting ID, it will save another data.
Another problem is when I remove dto.setId(id);, both method from lsnService; findById(id); and save(dto); are working! But as you can see, repo must update the entity but it won't!!!
So, I tried to put #Transactional in saving. And I even try putting Thread.sleep(5000); 5 secs delay between those two services. Like this,
lsnService.findById(id);
Thread.sleep(5000);
dto.setId(id);
lsnService.save(dto);
But it doesn't work either!
#Autowired
private LessonJpaRepository repo;
#Override
public LessonDto findById(Long id) {
// TODO Auto-generated method stub
Lesson lesson = repo.getOne(id);
LessonDto dto = new LessonDto(lesson);
return dto;
}
#Override
public void save(LessonDto dto) {
// TODO Auto-generated method stub
repo.save(dto.getEntity());
System.out.println(dto.getId()+dto.getTitle()+dto.getStructure()+dto.getExplanation());
}
And then, I check output of that dto. It's all there! repo is not saving it! It's so strange to me. Got any ideas?
public class LessonDto {
private Long id;
#NotNull(message = "Title must not be null")
#NotBlank(message = "Title must not be blank")
#ValidLessonTitle(message = "Title must begin with uppercase character")
private String title;
#NotNull(message = "Structure must not be null")
#NotBlank(message = "Structure must not be blank")
private String structure;
#NotNull(message = "Explanation must not be null")
#NotBlank(message = "Explanation must not be blank")
private String explanation;
public LessonDto() {
}
public LessonDto(Lesson lesson) {
this.id=lesson.getId();
this.title=lesson.getTitle();
this.structure=lesson.getStructure();
this.explanation=lesson.getExplanation();
}
#java.beans.Transient
public Lesson getEntity() {
Lesson lesson = new Lesson();
lesson.setId(this.id);
lesson.setTitle(this.title);
lesson.setStructure(this.structure);
lesson.setExplanation(this.explanation);
return lesson;
}
//getters and setters
}
This is the entity
#Entity
public class Lesson implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2239534946567783017L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "title")
private String title;
#Column(name = "structure")
private String structure;
#Column(name = "explanation")
private String explanation;
//getters and setters
}
There are 2 ways to make that work
Update instance that is retured by findById with values from DTO
Dont use findById as it fetches entity pointer (at least) to the cache and this might be the origin of problems. Try to use existsById instead

How to cache only when the json is valid

I have a spring rest api application that is using HATEOAS/PagingAndSortingRepository to do most of the heavy lifting.
I have implemented caching using guava but I am having issues where when the user cancels the request midway through an api call, it caches the incomplete json and re-serves it for 60 seconds.
I am trying to use the unless="" parameter of the #Cacheable annotation. Previously, I just used unless="#result == null" but that does not handle incomplete or invalid json.
This does not seem to work either. So now I am trying to use com.google.gson.JsonParser to parse the result and invalidate if applicable.
Repository
#RepositoryRestResource(path = "products", collectionResourceRel = "products")
public interface ProductEntityRepository extends PagingAndSortingRepository<ProductEntity, String> {
JsonParser parser = new JsonParser();
#Cacheable(value = CacheConfig.STORE_CACHE)
ProductEntity findByName(String name);
}
Cache Config
public final static String PRODUCTS_CACHE = "products";
#Bean
public Cache productsCache() {
return new GuavaCache(PRODUCTS_CACHE, CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build());
}
How do I detect invalid json in the unless="" parameter?
I figured out my own issue!
When I interrupted the api request to localhost/products and re-requested, I finally saw an error about not being able to fetch a onetomany mapping. I believe the error was lazy initialization error for a collection.
I solved this issue by adding #LazyCollection(LazyCollectionOption.FALSE) to my models where the #OneToMany and #ManyToOne mappings were decalared.
For example:
#Entity(name = "product")
#Table(name = "products", schema = "${DB_NAME}", catalog = "")
public class ProductEntity {
private Integer id;
private String name;
private List shipments = new ArrayList<>();
#Id
#Column(name = "id", nullable = false)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name", nullable = false, length = 10)
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "shipmentID", targetEntity=ShipmentEntity.class)
#LazyCollection(LazyCollectionOption.FALSE)
public Collection<ShipmentEntity> getShipments() { return shipments; }
public void setShipments(Collection<ShipmentEntity> shipments) { this.shipments = shipments; }
}

spring boot ignore field dynamically jpa

I am using Spring Boot REST Web Services and Angular 5 as a frontend, well I have a model class for hibernating like this :
#Entity
public class Title {
private Integer id;
private String name;
private Date releaseDate;
private Time runtime;
private String storyline;
private String picture;
private String rated;
private String type;
private Double rating;
private Integer numberOfVotes;
private Timestamp inserted;
private Set<Genre> genres = new HashSet<>();
private List<TitleCelebrity> titleCelebrities;
private List<TitleMedia> titleMedia;
// Basic getters and setter
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "title_genre", joinColumns = { #JoinColumn(name = "title_id") }, inverseJoinColumns = { #JoinColumn(name = "genre_id") })
public Set<Genre> getGenres() {
return genres;
}
public void setGenres(Set<Genre> genres) {
this.genres = genres;
}
#OneToMany(mappedBy = "title", cascade = CascadeType.ALL)
public List<TitleCelebrity> getTitleCelebrities() {
return titleCelebrities;
}
public void setTitleCelebrities(List<TitleCelebrity> titleCelebrities) {
this.titleCelebrities = titleCelebrities;
}
#OneToMany(mappedBy = "title", cascade = CascadeType.ALL)
public List<TitleMedia> getTitleMedia() {
return titleMedia;
}
public void setTitleMedia(List<TitleMedia> titleMedia) {
this.titleMedia = titleMedia;
}
}
And here's my REST controller
#RestController
#RequestMapping("titles")
#CrossOrigin(origins = {"http://localhost:4200"})
public class TitleController {
private TitleService titleService;
#Autowired
public void setTitleService(TitleService titleService) {
this.titleService = titleService;
}
// Api to get all the movies ordered by release date
#GetMapping("movies")
public List<Title> getAllMoviesOrderByReleaseDateDesc() {
return this.titleService.findByTypeOrderByReleaseDateDesc("movie");
}
#GetMapping("movies/{id}")
public Title findById(#PathVariable Integer id) {
return this.titleService.findById(id);
}
}
What I want is when I make a request to the first method '/movies' i don't want the collection of Telemedia, but if I make a request to the second method '/movies/id' i want the collection of Telemedia.
of course, the annotation #JsonIgnore will ignore the collection whatever the request is.
It may be better to create two models in this case; one to represent the first response and another to represent the second response.
You could also set the collection to null in your second request before sending it back.
You cannot accomplish this with #JsonIgnore alone as you cannot perform conditional logic in annotations.

How to show object's update history with Auditing?

I've got a problem, I made a CRUD in springboot with MYSQL and now I want to create a method which will return update history of my object...
I have class like:
#Entity
#Table
#EntityListeners(AuditingEntityListener.class)
#JsonIgnoreProperties(value = {"createdAt", "updatedAt"}, allowGetters = true)
#Audited
public class Note implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Getter
#Setter
private Long id;
#NotBlank
#Getter
#Setter
private String title;
#Version
#Getter
#Setter
private long version;
#NotBlank
#Getter
#Setter
private String content;
#Column(nullable = false, updatable = false)
#Temporal(TemporalType.TIMESTAMP)
#CreatedDate
#Getter
#Setter
private Date createdAt;
#Column(nullable = false)
#Temporal(TemporalType.TIMESTAMP)
#LastModifiedDate
#Getter
#Setter
private Date updatedAt;
}
But I don't know how can I now create a HTTP call to show that history of updates by #Audited.
I found something like this: Find max revision of each entity less than or equal to given revision with envers
But I don't know how to implement it in my project...
#RestController
#RequestMapping("/api")
public class NoteController
{
#Autowired
NoteRevisionService noteRevisionService;
#Autowired
NoteRepository noteRepository;
// Get All Notes
#GetMapping("/notes")
public List<Note> getAllNotes() {
return noteRepository.findAll();
}
// Create a new Note
#PostMapping("/notes")
public Note createNote(#Valid #RequestBody Note note) {
return noteRepository.save(note);
}
// Get a Single Note
#GetMapping("/notes/{id}")
public Note getNoteById(#PathVariable(value = "id") Long noteId) {
return noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
}
#GetMapping("/notes/{id}/version")
public List<?> getVersions(#PathVariable(value = "id") Long noteId)
{
return noteRevisionService.getNoteUpdates(noteId);
}
// Update a Note
#PutMapping("/notes/{id}")
public Note updateNote(#PathVariable(value = "id") Long noteId,
#Valid #RequestBody Note noteDetails) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
note.setTitle(noteDetails.getTitle());
note.setContent(noteDetails.getContent());
Note updatedNote = noteRepository.save(note);
return updatedNote;
}
// Delete a Note
#DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(#PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
noteRepository.delete(note);
return ResponseEntity.ok().build();
}
}
getVersions its the call of function which Joe Doe sent me.
There: Repository
#Repository
public interface NoteRepository extends JpaRepository<Note, Long>
{
}
You can use AuditQuery for this. The getNoteUpdates method below returns a list of mappings. Each mapping contains an object state and the time of the update that led to that state.
#Service
#Transactional
public class NoteRevisionService {
private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);
#PersistenceContext
private EntityManager entityManager;
#SuppressWarnings("unchecked")
public List<Map.Entry<Note, Date>> getNoteUpdates(Long noteId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
AuditQuery query = auditReader.createQuery()
.forRevisionsOfEntity(Note.class, false, false)
.add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
.add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications
List<Object[]> revisions = (List<Object[]>) query.getResultList();
List<Map.Entry<Note, Date>> results = new ArrayList<>();
for (Object[] result : revisions) {
Note note = (Note) result[0];
DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];
logger.info("The content of the note updated at {} was {}", revisionEntity.getRevisionDate(), note.getContent());
results.add(new SimpleEntry<>(note, revisionEntity.getRevisionDate()));
}
return results;
}
}
Note that if you can restrict the query somehow (for example by filtering on a property), you should definitely do it, because otherwise performing the query can have a negative impact on the performance of your entire application (the size of the returned list might be huge if this object was often updated).
Since the class has been annotated with the #Service annotation, you can inject/autowire NoteRevisionService like any other regular Spring bean, particularly in a controller that handles a GET request and delegates to that service.
UPDATE
I didn't know that extra steps had to be taken to serialize a list of map entries. There may be a better solution but the following approach gets the job done and you can customize the format of the output revisionDate with a simple annotation.
You need to define another class, say NoteUpdatePair, like so:
public class NoteUpdatePair {
private Note note;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date revisionDate; // this field is of type java.util.Date (not java.sql.Date)
NoteUpdatePair() {}
public NoteUpdatePair(Note note, Date revisionDate) {
this.note = note;
this.revisionDate = revisionDate;
}
public Note getNote() {
return note;
}
public void setNote(Note note) {
this.note = note;
}
public Date getRevisionDate() {
return revisionDate;
}
public void setRevisionDate(Date revisionDate) {
this.revisionDate = revisionDate;
}
}
and now, instead of returning a list of map entries, you'll return a list of NodeUpdatePair objects:
#Service
#Transactional
public class NoteRevisionService {
private static final Logger logger = LoggerFactory.getLogger(NoteRevisionService.class);
#PersistenceContext
private EntityManager entityManager;
#SuppressWarnings("unchecked")
public List<NoteUpdatePair> getNoteUpdates(Long noteId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
AuditQuery query = auditReader.createQuery()
.forRevisionsOfEntity(Note.class, false, false)
.add(AuditEntity.id().eq(noteId)) // if you remove this line, you'll get an update history of all Notes
.add(AuditEntity.revisionType().eq(RevisionType.MOD)); // we're only interested in MODifications
List<Object[]> revisions = (List<Object[]>) query.getResultList();
List<NoteUpdatePair> results = new ArrayList<>();
for (Object[] result : revisions) {
Note note = (Note) result[0];
DefaultRevisionEntity revisionEntity = (DefaultRevisionEntity) result[1];
logger.info("The content was {}, updated at {}", note.getContent(), revisionEntity.getRevisionDate());
results.add(new NoteUpdatePair(note, revisionEntity.getRevisionDate()));
}
return results;
}
}
Regarding your question about the service's usage, I can see that you've already autowired it into your controller, so all you need to do is expose an appropriate method in your NoteController:
#RestController
#RequestMapping("/api")
public class NoteController {
#Autowired
private NoteRevisionService revisionService;
/*
the rest of your code...
*/
#GetMapping("/notes/{noteId}/updates")
public List<NoteUpdatePair> getNoteUpdates(#PathVariable Long noteId) {
return revisionService.getNoteUpdates(noteId);
}
}
Now when you send a GET request to ~/api/notes/1/updates (assuming nodeId is valid), the output should be properly serialized.

Enhanced Spring Data Rest delivers empty relations

in my current implementation using Spring-Boot, -HATEOAS, -Rest-Data I'm trying to spare some further rest calls and enhance my rest resource for credits to also deliver relations of a credit (see below account as ManyToOne and creditBookingClassPayments as OneToMany).
The problem now is that I'm not able to get it run. The call always delivers empty relations. I really would appreciate some help on this.
Here are the surroundings:
Credit.java
#Entity
#Getter
#Setter
public class Credit {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Setter(NONE)
#Column(name = "id")
private Long itemId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="account_id", nullable = false)
private Account account;
#OneToMany(mappedBy = "credit")
private List<CreditBookingClassPayment> creditBookingClassPayments = new ArrayList<>();
#NotNull(message="Please enter a valid short name.")
#Column(length = 10, nullable = false)
private String shortName;
#NotNull(message="Please enter a valid name.")
#Column(nullable = false)
private String name;
...
}
CreditRepositoryCustomImpl.java
uses QueryDsl to enhance the credit resource with its realation
...
#Override
public List<Credit> findDistinctByAccountItemIdNew(Long accountId) {
QCredit credit = QCredit.credit;
QAccount account = QAccount.account;
QCreditBookingClassPayment creditBookingClassPayment = QCreditBookingClassPayment.creditBookingClassPayment;
QBookingClass bookingClass = QBookingClass.bookingClass;
BooleanExpression hasAccountItemId = credit.account.itemId.eq(accountId);
List<Credit> credits = from(credit).where(hasAccountItemId)
.innerJoin(credit.account, account)
.leftJoin(credit.creditBookingClassPayments, creditBookingClassPayment)
.leftJoin(creditBookingClassPayment.bookingClass, bookingClass).groupBy(credit.itemId).fetch();
return credits;
}
...
CreditController.java
looking into responseBody here all (account and credit payments) is available for credits
#RepositoryRestController
public class CreditController {
#Autowired
private CreditRepository creditRepository;
#RequestMapping(value = "/credit/search/findAllByAccountItemIdNew", method= RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#ResponseBody
public ResponseEntity<Resources<PersistentEntityResource>> findAllByAccountItemIdNew(#RequestParam Long accountId, PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
List<Credit> credits = creditRepository.findDistinctByAccountItemIdNew(accountId);
Resources<PersistentEntityResource> responseBody = new Resources<PersistentEntityResource>(credits.stream()
.map(persistentEntityResourceAssembler::toResource)
.collect(Collectors.toList()));
return ResponseEntity.ok(responseBody);
}
}
CreditResourceIntegrTest.java
here creditResourcesEntity hold the credit but account is null and creditBookingClassPayment is an empty array
#Test
public void testFindAllByAccountItemId() throws URISyntaxException {
URIBuilder builder = new URIBuilder(creditFindAllByAccountItemIdRestUrl);
builder.addParameter("accountId", String.valueOf(EXPECTED_ACCOUNT_ID));
builder.addParameter("projection", "base");
RequestEntity<Void> request = RequestEntity.get(builder.build())
.accept(MediaTypes.HAL_JSON).acceptCharset(Charset.forName("UTF-8")).build();
ResponseEntity<Resources<Resource<Credit>>> creditResourcesEntity =
restTemplate.exchange(request, new ParameterizedTypeReference<Resources<Resource<Credit>>>() {});
assertEquals(HttpStatus.OK, creditResourcesEntity.getStatusCode());
//assertEquals(EXPECTED_CREDIT_COUNT, creditResourcesEntity.getBody().getContent().size());
}
Do I miss something?
Thanks for your help!
Karsten
Okay, PersistentEntityResourceAssembler doesn't support relations. But this could be handled by using projections.
CreditProjection.java
#Projection(name = "base" , types = Credit.class)
public interface CreditProjection {
String getShortName();
String getName();
List<CreditBookingClassPaymentProjection> getCreditBookingClassPayments();
BigDecimal getValue();
BigDecimal getInterestRate();
BigDecimal getMonthlyRate();
}
CreditBookingClassPaymentProjection.java
#Projection(name = "base" , types = CreditBookingClassPayment.class)
public interface CreditBookingClassPaymentProjection {
BookingClass getBookingClass();
CreditPaymentType getCreditPaymentType();
}
CreditController.java
#RepositoryRestController
public class CreditController {
#Autowired
private ProjectionFactory projectionFactory;
#Autowired
private CreditRepository creditRepository;
#RequestMapping(value = "/credit/search/findAllByAccountItemIdNew", method = RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#ResponseBody
public ResponseEntity<Resources<?>> findAllByAccountItemIdNew(#RequestParam Long accountId,
PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
List<Credit> credits = creditRepository.findDistinctByAccountItemIdNew(accountId);
List<PersistentEntityResource> creditResources = new ArrayList<>();
for (Credit credit : credits) {
// credit.getCreditBookingClassPayments()
PersistentEntityResource creditResource = persistentEntityResourceAssembler.toResource(credit);
creditResources.add(creditResource);
}
Resources<CreditProjection> responseBody = new Resources<CreditProjection>(credits.stream()
.map(credit -> projectionFactory.createProjection(CreditProjection.class, credit))
.collect(Collectors.toList()));
return ResponseEntity.ok(responseBody);
}
}

Resources