How to implement Auditing with DynamoDB in Spring Boot? - java

I've dynamoDB database, in which I've a table named user. I want to log auditing (CreatedBy, LastModifiedBy, CreatedDate, LastModifiedDate)in this table.
I've found JPA auditing (from here) and MongoDB auditing with annotations #EnableJpaAuditing and #EnableMongoAuditing respectively. But obviously they are not working with dynamoDB.
Here is my abstract class for auditing:
#DynamoDBDocument
public abstract class PQSSAbstractAuditingEntity implements Serializable{
#CreatedBy
#JsonIgnore
#DynamoDBAttribute
private String createdBy;
#LastModifiedBy
#JsonIgnore
#DynamoDBAttribute
private String lastModifiedBy;
#CreatedDate
#JsonIgnore
#DynamoDBAttribute
private Date createdDate;
#LastModifiedDate
#JsonIgnore
#DynamoDBAttribute
private Date lastModifiedDate = new Date();
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Date getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
I've assigned dates to respective fields but I've to set createdBy and lastModifiedBy depending on the logged in user. So I've to fetch the dynamically at runtime, when ever new entry is added into the database.
I know how to set these fields statically but the problem is how to make annotations aware of these changes at run time.
As I mentioned, I've found AuditAware for JPA and mongo. I need the same for dynamoDB.
Any help will be appreciated. As I'm new to Spring boot.

The question is already some years old but if somebody has also that problem, following solution works..
The problem is, that #EnableDynamoDBAuditing and #EnableDynamoDBRepositories don't work properly together. To solve this problem you have to add both annotations to you configuration class, create AuditorAware and DateTimeProvider beans and add all your entities/documents to your DynamoDBMappingContext manually.
PersistenceConfiguration.java
#Configuration
#EnableDynamoDBAuditing(auditorAwareRef = "userAuditing", dateTimeProviderRef = "dateAuditing")
#EnableDynamoDBRepositories(basePackages = "your.repository.package.name")
public class PersistenceConfiguration {
#Bean
public AuditorAware<String> userAuditing() {
return () -> Optional.of("TestUser"); //get username from SecurityContext
}
#Bean
public DateTimeProvider dateAuditing() {
return CurrentDateTimeProvider.INSTANCE;
}
#Bean
public DynamoDBMappingContext dynamoDBMappingContext() {
DynamoDBMappingContext mappingContext = new DynamoDBMappingContext();
//add your 'entities' manually
mappingContext.getPersistentEntity(YourEntity.class);
return mappingContext;
}
// do further configuration stuff...
}
YourEntity.java
#DynamoDBTable(tableName = "YourEntity")
public class YourEntity {
#CreatedDate
#DynamoDBAttribute
#DynamoDBTypeConverted(converter = LocalDateTimeConverter.class)
private LocalDateTime createdOn;
#CreatedBy
#DynamoDBAttribute
private String createdBy;
#LastModifiedDate
#DynamoDBAttribute
#DynamoDBTypeConverted(converter = LocalDateTimeConverter.class)
private LocalDateTime updatedOn;
#LastModifiedBy
#DynamoDBAttribute
private String updatedBy;
// add further properties...
}
I know that there are some other solutions like #DynamoDBAutoGeneratedTimestamp and the usage of their strategies, but in my mind that's the cleanest solution regarding the use of spring.

The annotaion #DynamoDBAutoGeneratedTimestamp can be used along with DynamoDBAutoGenerateStrategy to audit the item.
Strategy CREATE (Use this for Create audit):-
#DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE)
public Date getCreatedDate() { return createdDate; }
public void setCreatedDate(Date createdDate) { this.createdDate = createdDate; }
Strategy ALWAYS (Use this if you want last modified date):-
#DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.ALWAYS)
public Date getLastUpdatedDate() { return lastUpdatedDate; }
public void setLastUpdatedDate(Date lastUpdatedDate) { this.lastUpdatedDate = lastUpdatedDate; }
If you want both create timestamp and last modified timestamps, please create two different attributes. One attribute should use CREATE strategy and other one should use ALWAYS strategy.
AutoGeneratedTimeStamp

Related

spring boot mongodb audit gives duplicate collection error

spring boot mongodb audit gives duplicate collection error. I'm trying to create dateCreate and dateUpdate fields when I insert and update a collection but when updated it gives the error:
org.springframework.dao.DuplicateKeyException: Write operation error
on server user.domain.com:27017. Write error: WriteError{code=11000,
message='E11000 duplicate key error collection: springboot.category
index: id dup key: { _id: "21" }', details={}}.
the execution is duplicating the key, below is my structure
My class AuditingConfig.java:
Configuration
#EnableMongoAuditing
public class AuditingConfig {
#Bean
public AuditorAware<String> myAuditorProvider() {
return new AuditorAwareImpl();
}
}
My class AuditMetadata.java:
#Setter
#Getter
public class AuditMetadata {
#CreatedDate
private LocalDateTime createdDate;
#LastModifiedDate
private LocalDateTime lastModifiedDate;
#Version
private Long version;
// #CreatedBy
// private String createdByUser;
// #LastModifiedBy
// private String modifiedByUser;
//...getters and setters omitted
}
My class AuditorAwareImpl.java:
public class AuditorAwareImpl implements AuditorAware<String> {
#Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Admin");
}
}
My class entity CategoryMongo.java:
Getter
#Setter
#NoArgsConstructor
#Document(collection = "category")
public class CategoryMongo extends AuditMetadata{
#Id
#JsonProperty("category_id")
private String category_id;
#JsonProperty("id_colletion")
private String emberId;
public String getEmberId() {
return category_id;
}
#JsonProperty("category_name")
private String name;
#JsonProperty("category_active")
private ProductEnum active = ProductEnum.ativo;
#JsonProperty("category_slug")
private String slug;
// #JsonProperty("category_updateAt")
// #LastModifiedDate
// private Date updateAt;
// #JsonProperty("category_createdAt")
// #CreatedDate
// private Date createdAt;
}
My method save:
CategoryMongo catm = new CategoryMongo();
catm.setName(category.getName());
catm.setSlug(category.getSlug());
catm.setActive(category.getActive());
catm.setCategory_id(category.getCategory_id().toString());
catm.setEmberId(category.getCategory_id().toString());
categoryRepositoryMongo.save(catm);
SOLVED
I solved the error I use in the document an interface follows the updated classes:
class AuditMetadata.java:
#Setter
#Getter
public class AuditMetadata {
#CreatedDate
private LocalDateTime createdDate;
#LastModifiedDate
private LocalDateTime lastModifiedDate;
#Version
private Long version;
protected boolean persisted;
// #CreatedBy
// private String createdByUser;
// #LastModifiedBy
// private String modifiedByUser;
//...getters and setters omitted
}
class Document CategoryMongo.java:
#Getter
#Setter
#NoArgsConstructor
#Document(collection = "category")
public class CategoryMongo extends AuditMetadata implements Persistable<String>{
#Id
#JsonProperty("category_id")
private String category_id;
#JsonProperty("id_colletion")
private String emberId;
public String getEmberId() {
return category_id;
}
#JsonProperty("category_name")
private String name;
#JsonProperty("category_active")
private ProductEnum active = ProductEnum.ativo;
#JsonProperty("category_slug")
private String slug;
#Override
#Nullable
public String getId() {
return category_id;
}
#Override
public boolean isNew() {
return !persisted;
}
// #JsonProperty("category_updateAt")
// #LastModifiedDate
// private Date updateAt;
// #JsonProperty("category_createdAt")
// #CreatedDate
// private Date createdAt;
}
save method:
CategoryMongo catm = new CategoryMongo();
catm.setName(category.getName());
catm.setSlug(category.getSlug());
catm.setActive(category.getActive());
catm.setCategory_id(category.getCategory_id().toString());
catm.setPersisted(true);
categoryRepositoryMongo.save(catm);
But something happens that I didn't want to happen: the #CreatedDate field disappears when I update it and only #LastModifiedDate appears in the result. if anyone knows how to solve this post here

repoistory.save() getting invoked with invalid entry when unit testing

I'm using java validation API to validate fields in my Note class:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "note")
public class Note {
#Id
#Column(name = "id", nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "date", columnDefinition = "DATE")
private LocalDate date;
#NotBlank(message = "Enter a topic")
#Column(name = "topic")
private String topic;
#NotBlank(message = "Content can't be empty")
#Column(name = "content")
private String content;
#Column(name = "type")
private NoteType noteType;
#NotNull
#ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinColumn(name = "user_id")
#JsonIgnore
private User user;
}
NoteService:
#Service
#AllArgsConstructor
public class NoteService {
#Autowired
private NoteRepository noteRepository;
#Autowired
private UserRepository userRepository;
public void addNote(#Valid Note note) {
note.setUser(getLoggedInUser());
if (validateNote(note)) {
noteRepository.save(note);
}
}
public List<Note> getNotes() {
return getLoggedInUser().getNotes();
}
public Note editNote(Note newNote, Long id) {
noteRepository.editNoteById(newNote, id);
return newNote;
}
public List<Note> getNotesByTopic(String topic) {
List<Note> notes = noteRepository.getNotesByTopicAndUser(topic, getLoggedInUser());
return notes;
}
public boolean validateNote(Note note) {
return validateNoteType(note.getNoteType())
&& note.getDate() != null;
}
public boolean validateNoteType(NoteType type) {
return type.equals(NoteType.NOTE)
|| type.equals(NoteType.SKILL);
}
public User getLoggedInUser() {
return userRepository.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName());
}
}
Test:
#ExtendWith(MockitoExtension.class)
#ExtendWith(SpringExtension.class)
class NoteServiceTest {
#Mock
private NoteRepository noteRepositoryMock;
#Mock
private UserRepository userRepositoryMock;
#Mock
SecurityContext mockSecurityContext;
#Mock
Authentication authentication;
private NoteService noteService;
#BeforeEach
void setUp() {
noteService = new NoteService(noteRepositoryMock, userRepositoryMock);
Mockito.when(mockSecurityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(mockSecurityContext);
}
#Test
void shouldAddNote() {
LocalDate date = LocalDate.now();
Note note = new Note(0L, date, "test", "", NoteType.NOTE, null);
noteService.addNote(note);
Mockito.verify(noteRepositoryMock).save(note);
}
}
The field user in the Note class is annotated with #NotNull and I'm passing a null user to this note but the note is still getting saved. Same thing when I pass an empty string. Any idea why that is happening? I'm new to unit testing
I'm new to unit testing - your perfectly valid question has nothing to do with unit testing.
#NotNull does nothing on it own. Its actually a contract stating the following:
A data member (or anything else annotated with #NotNull like local variables, and parameters) can't be should not be null.
For example, instead of this:
/**
* #param obj should not be null
*/
public void MyShinyMethod(Object obj)
{
// Some code goes here.
}
You can write this:
public void MyShinyMethod(#NotNull Object obj)
{
// Some code goes here.
}
P.S.
It is usually appropriate to use some kind of annotation processor at compile time, or something that processes it at runtime. But I don't really know much about annotation processing. But I am sure Google knows :-)
You need to activate validation on you service class with the #Validated annotation so the validation of parameters kicks in.
#Service
#AllArgsConstructor
#Validated
public class NoteService {
...
See Spring #Validated in service layer and Spring Boot: How to test a service in JUnit with #Validated annotation? for more details.
If for some reason you need to manually perform the validation you can always do something like this:
#Component
public class MyValidationImpl {
private final LocalValidatorFactoryBean validator;
public MyValidationImpl (LocalValidatorFactoryBean validator) {
this.validator = validator;
}
public void validate(Object o) {
Set<ConstraintViolation<Object>> set = validator.validate(o);
if (!set.isEmpty()) {
throw new IllegalArgumentException(
set.stream().map(x -> String.join(" ", x.getPropertyPath().toString(), x.getMessage())).collect(
Collectors.joining("\n\t")));
}
}
}
So your noteRepository is Mocked, so you it's not actually calling save on your repository.
Mockito.verify(noteRepositoryMock).save(note);
All you are verifying here is that a call to save is made, not that it was successful.

Adding an object to a List of another type

I'm trying to return the record that I got from my database. But I'm having a problem on how I can do that because the data than I retrieved from the database is in a different class from the return parameter.
public List<Record> getRecord(List<Request> requests) {
List<Record> records = new ArrayList<>();
for (Request request : requests) {
Billing billing = billingRepository
.findByBillingCycleAndStartDateAndEndDate(
request.getBillingCycle()
, request.getStartDate()
, request.getEndDate());
if (billing != null) {
// Need to add "billing" to "records" list here
}
}
return records;
}
Record.class
public class Record {
private int billingCycle;
private LocalDate startDate;
private LocalDate endDate;
private String accountName;
private String firstName;
private String lastname;
private double amount;
public Record() {
}
//Getters and setters
Billing.class
public class Billing {
private int billingId;
private int billingCycle;
private String billingMonth;
private Double amount;
private LocalDate startDate;
private LocalDate endDate;
private String lastEdited;
private Account accountId;
public Billing() {
}
//Getters and setters
What can I do? and please explain the answer so I can understand it. I really want to learn
You can use DozerMapper. It will map the object to another object having same name properties or you have to write the mapping in the dozer-mapping xml.
Lets come to your question. Here you are trying to convert your entity to another object.
For that you have to write mapping code. It will be something like this and it is very common practice to convert entity objects to another object before using them.
Record toRecord(Billing billing) {
if(billing == null) {
return null;
}
Record record = new Record();
record.setBillingCycle = billing.getBillingCycle();
...
...
// other properties
...
return record;
}

How to deserialize a String to a Date with Morphia

I have a Mongo collection with objects of this format:
{
id: 1,
date: "2020-08-06T12:00:00Z",
...
}
I have Java code that needs to read from this collection but never writes to it. The process that writes to this collection is not owned by me so I can't necessarily change the format of that date string. I initially tried to model my Java Morphia object like this:
#Entity public class MyDocument {
#Id
private Integer id;
private Date date;
...
}
This did not work because Morphia didn't know how to deserialize that date format into a Date object. The solution that I came up with was treating the date as a String on the POJO and then having a getDate() method that did the actual deserialization. I am wondering, is there a better way for me to do this? I know if you're using Jackson you can annotate certain fields with #JsonDeserialize and pass a deserializer so I was wondering if there was something similar for Morphia.
My solution (which feels suboptimal to me):
#Entity public class MyDocument {
#Id
private Integer id;
private String date;
...
private Date getDate() {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
try {
return dateFormat.parse(date);
} catch (Exception ex) {
return null;
}
}
}
You can go ahead and create a simple converter extending the TypeConverter like so:
public class DateConverter extends TypeConverter {
private static final String FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private final SimpleDateFormat simpleDateFormat;
public DateConverter() {
super(Date.class);
this.simpleDateFormat = new SimpleDateFormat(FORMAT);
}
#Override
public Object decode(Class<?> targetClass, Object fromDBObject, MappedField optionalExtraInfo) {
try {
return simpleDateFormat.parse(((String) fromDBObject));
} catch (ParseException e) {
return null;
}
}
}
The go ahead and register your formatter for your document entity like so:
#Entity("Documents")
#Converters(DateConverter.class)
public class Document {
#Id
private Integer id;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
private Date date;
public Date getDate() { return date; }
public void setDate(Date date) { this.date = date; }
#Override
public String toString() {
return "Document{" +
"id=" + id +
", date=" + date +
'}';
}
}
This will effectively tell Morphia to decode the database incoming values via parsing the string with the desired pattern, resulting directly into a concrete Date object without any additional conversion logic.

Is it possible to change the type of a field when storing an object in MongoDB using Morphia?

I have a class named Meeting that contains some fields, one of them is timeStamp and its type is long. I want to store meetings in MongoDb, but I want this timeStamp to be stored as a Date. Is there a way to specify Morphia to store it using a different type with a mapper/convertor function?
#Entity(noClassnameStored = true)
public class Meeting {
private String entity1;
private String entity2;
private long timeStamp;
public Meeting(){
};
public Meeting(String entity1, String entity2, long timeStamp) {
this.entity1 = entity1;
this.entity2 = entity2;
this.timeStamp = timeStamp;
}
public String getEntity1() {
return entity1;
}
public String getEntity2() {
return entity2;
}
public long getTimeStamp() {
return timeStamp;
}
}
You can use #PrePersist and #PostLoad to coerce the types. See here for more.

Categories