Audit fields of a class using a class level annotation (aspectj) - java

I'm writing a simple auditing framework with aspectj, which allows me to audit the fields of a class which are annotated with an #Audit annotation.
As value the #Audit annotation expects an array of field names to be watched
Example Usage:
#Audit({"name","phoneNumber"})
class User {
private String name;
private String phoneNumber;
public getName(){
return name;
};
public setName(String name){
this.name=name;
}
}
How does the Aspect look that watches the assignment of fields that are annotated like in the above example?
Here the stub of my first try:
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.TYPE})
public #interface Audit {
String[] value()
}
#Aspect
class AuditAspect {
#Pointcut("????")
public void markedFieldWasModified(){}
#AfterReturning("markedFieldWasModified()")
public void addFieldToModifiedFields(JoinPoint jp, AuditableEO eo){
eo.addModifiedField(jp.getSignature().getName());
}
// inter Type declarations
public interface IAuditableEO {
public Iterator<String> modifiedFields();
public boolean modified();
public boolean addModifiedField(String field);
};
}

according to https://eclipse.org/aspectj/doc/next/quick5.pdf
you should be able to do set(* *.*) && #target(Audit)
you then have to check the joinpoint if an auditable field is being modified.

How about not over-engineering the whole thing and directly annotating fields instead of classes? You can also skip the IAuditableEO interface IMO, I cannot see why it would be useful. Here is a simple example similar to yours, just with the aspect in code-style syntax (I prefer it to annotation-style syntax for clarity, but you can easily convert it by yourself):
Audit annotation for fields (not classes):
package de.scrum_master.app;
import java.lang.annotation.*;
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD})
public #interface Audit {}
User class with a sample main method:
package de.scrum_master.app;
public class User {
private int id;
#Audit private String name;
#Audit private String phoneNumber;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
public static void main(String[] args) {
User user = new User();
user.setId(11);
user.setName("John Doe");
user.setPhoneNumber("+49-1111-23456789");
System.out.println("User(" + user.getId() + ", " + user.getName() + ", " + user.getPhoneNumber() + ")");
}
}
Audit aspect:
package de.scrum_master.aspect;
import de.scrum_master.app.Audit;
public aspect AuditAspect {
pointcut fieldModification() : set(#Audit * *);
after() : fieldModification() {
System.out.println(thisJoinPointStaticPart);
}
}
Sample output:
set(String de.scrum_master.app.User.name)
set(String de.scrum_master.app.User.phoneNumber)
User(11, John Doe, +49-1111-23456789)
As you can see, only the annotated fields are caught, not the ID field. This permits for fine-granular auditing on a per-field basis. Furthermore in the advide you have everything you need if you want to record anything in and audit database: field type and name, class name and so forth.

Related

Spring boot : non-entity bean validation not working

I have normal Java POJO class, which is not an entity class, named Student. I am using spring boot validator. within my REST controller when, for a request just a student object will be returned. I have set different validation in the Student class. But the validation is not working. I have given the age limit 10 to 30. But it is creating Object with 35 years old. Is there any way to make this validation work? Note : i am doing this as test, if this validation work, i will use it within my main project.
Student
package com.mkyong;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
#Validated
public class Student {
#NotEmpty
private String id;
#NotEmpty
private String name;
#Min(10)
#Max(30)
private int age;
public Student() {
}
public Student(String id, String name, #Min(10) #Max(30) int age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge( #Min(10) #Max(30) int age) {
this.age = age;
}
}
**Rest Controller: **
public class BookController {
// Find
#GetMapping("/student")
Student studentCreate() {
Student student = new Student("","",35);
student.setAge(36);
return student;
}
}
Your entity will be validated automatically in case that your request body argument will be annotated with #Valid annotation
You can validate your entities manually by using: javax.validation.Validator
Following your case:
Inject Validator in BookController.
call Validator#validate using your DTO as a parameter.
Check if there are viloations
public class BookController {
#Autowired
Validator validator;
// Find
#GetMapping("/student")
Student studentCreate() {
Student student = new Student("","",35);
student.setAge(36);
Set<ConstraintViolation<Student>> result = validator.validate(student);
if (!result.isEmpty()) {
//do here whatever you want with each validation violation.
}
return student;
}
}
For more details check official docs: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation
Note:
Depends on your version of spring boot, org.springframework.boot:spring-boot-starter-validation dependency might be missing from your dependencies, so double check if it exists.

Lombok's builder with mandatory parameters

If I add #Builder to a class. The builder method is created.
Person.builder().name("john").surname("Smith").build();
I have a requirement where a particular field is mandatory. In this case, the name field is mandatory. Ideally, I would like to declare it like so.
Person.builder("john").surname("Smith").build();
When googling i found many alternatives like overriding the builder implementation as below:
#Builder
public class Person {
private String name;
private String surname;
public static PersonBuilder builder(String name) {
return new PersonBuilder().name(name);
}
}
And then use it like below:
Person p = Person.builder("Name").surname("Surname").build();
The problem with above approach is that it still provides the name() and PersonBuilder() method like below, which i don't want:
Person p = Person.builder("Name").surname("Surname").name("").build();
Person p = new Person.PersonBuilder().build;
Another approach is to add #lombok.nonnull check at name which will force to provide value for name while creating object. but it is a runtime check. it will not force me to provide value for name while creating object.
Is there any additional technique which lombok provides to achieve below:
Person p = Person.builder("Name").surname("Surname").build();
Note: The builder() and name() should not be exposed. The only way to create Person object should be either above or below:
Person p = Person.builder("Name").build();
You can't really do it with lombok, see the explanation from the library authors. But is it that complicated to roll this builder on your own?
public static class PersonBuilder {
private final String name;
private String surname;
PersonBuilder(String name) {
this.name = name;
}
public PersonBuilder surname(String surname) {
this.surname = surname;
return this;
}
public Person build() {
return new Person(name, surname);
}
}
with the same method that you already have:
public static PersonBuilder builder(String name) {
return new PersonBuilder(name);
}
Try to make the builder private.
Did you check this comment Required arguments with a Lombok #Builder
I am pretty sure you will find out once read the thread one more time.
P.S. If you have a class with only two field better use directly a constructor.
Best Practice:
import lombok.Builder;
import lombok.NonNull;
#Builder(builderMethodName = "privateBuilder")
public class Person {
#NonNull
private String name;
private String surname;
public static class PersonNameBuilder {
public PersonBuilder name(String name) {
return Person.privateBuilder().name(name);
}
}
private static class PersonExtraBuilder extends PersonBuilder{
#Deprecated
#Override
public PersonBuilder name(String name) {
return this;
}
}
public static PersonNameBuilder builder(String name) {
return new PersonNameBuilder();
}
private static PersonExtraBuilder privateBuilder(){
return new PersonExtraBuilder();
}
}
Usage:
PersonNameBuilder nameBuilder = Person.builder();
PersonBuilder builder = nameBuilder.name("John");
Person p1 = builder.surname("Smith").build();
// Or
Person p2 = Person.builder().name("John").surname("Smith").build();
// The last `.name("")` will not work, and it will be marked as Deprecated by IDE.
Person p3 = Person.builder().name("John").surname("Smith").name("").build();

Id property is missing in the reponse of a RestAPI call [duplicate]

Have a strange problem and can't figure out how to deal with it.
Have simple POJO:
#Entity
#Table(name = "persons")
public class Person {
#Id
#GeneratedValue
private Long id;
#Column(name = "first_name")
private String firstName;
#Column(name = "middle_name")
private String middleName;
#Column(name = "last_name")
private String lastName;
#Column(name = "comment")
private String comment;
#Column(name = "created")
private Date created;
#Column(name = "updated")
private Date updated;
#PrePersist
protected void onCreate() {
created = new Date();
}
#PreUpdate
protected void onUpdate() {
updated = new Date();
}
#Valid
#OrderBy("id")
#OneToMany(mappedBy = "person", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PhoneNumber> phoneNumbers = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Date getCreated() {
return created;
}
public Date getUpdated() {
return updated;
}
public List<PhoneNumber> getPhoneNumbers() {
return phoneNumbers;
}
public void addPhoneNumber(PhoneNumber number) {
number.setPerson(this);
phoneNumbers.add(number);
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
#Entity
#Table(name = "phone_numbers")
public class PhoneNumber {
public PhoneNumber() {}
public PhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
#Id
#GeneratedValue
private Long id;
#Column(name = "phone_number")
private String phoneNumber;
#ManyToOne
#JoinColumn(name = "person_id")
private Person person;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
and rest endpoint:
#ResponseBody
#RequestMapping(method = RequestMethod.GET)
public List<Person> listPersons() {
return personService.findAll();
}
In json response there are all fields except Id, which I need on front end side to edit/delete person. How can I configure spring boot to serialize Id as well?
That's how response looks like now:
[{
"firstName": "Just",
"middleName": "Test",
"lastName": "Name",
"comment": "Just a comment",
"created": 1405774380410,
"updated": null,
"phoneNumbers": [{
"phoneNumber": "74575754757"
}, {
"phoneNumber": "575757547"
}, {
"phoneNumber": "57547547547"
}]
}]
UPD Have bidirectional hibernate mapping, maybe it's somehow related to issue.
I recently had the same problem and it's because that's how spring-boot-starter-data-rest works by default. See my SO question -> While using Spring Data Rest after migrating an app to Spring Boot, I have observed that entity properties with #Id are no longer marshalled to JSON
To customize how it behaves, you can extend RepositoryRestConfigurerAdapter to expose IDs for specific classes.
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
#Configuration
public class RepositoryConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(Person.class);
}
}
In case you need to expose the identifiers for all entities:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Type;
#Configuration
public class RestConfiguration implements RepositoryRestConfigurer {
#Autowired
private EntityManager entityManager;
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(
entityManager.getMetamodel().getEntities().stream()
.map(Type::getJavaType)
.toArray(Class[]::new));
}
}
Note that in versions of Spring Boot prior to 2.1.0.RELEASE you must extend the (now deprecated) org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter instead of implement RepositoryRestConfigurer directly.
If you only want to expose the identifiers of entities that extends or
implements specific super class or interface:
...
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(
entityManager.getMetamodel().getEntities().stream()
.map(Type::getJavaType)
.filter(Identifiable.class::isAssignableFrom)
.toArray(Class[]::new));
}
If you only want to expose the identifiers of entities with a specific annotation:
...
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(
entityManager.getMetamodel().getEntities().stream()
.map(Type::getJavaType)
.filter(c -> c.isAnnotationPresent(ExposeId.class))
.toArray(Class[]::new));
}
Sample annotation:
import java.lang.annotation.*;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface ExposeId {}
Answer from #eric-peladan didn't work out of the box, but was pretty close, maybe that worked for previous versions of Spring Boot. Now this is how it is supposed to be configured instead, correct me if I'm wrong:
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;
#Configuration
public class RepositoryConfiguration extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(User.class);
config.exposeIdsFor(Comment.class);
}
}
The class RepositoryRestConfigurerAdapter has been deprecated since 3.1, implement RepositoryRestConfigurer directly.
#Configuration
public class RepositoryConfiguration implements RepositoryRestConfigurer {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(YouClass.class);
RepositoryRestConfigurer.super.configureRepositoryRestConfiguration(config);
}
}
Font: https://docs.spring.io/spring-data/rest/docs/current-SNAPSHOT/api/org/springframework/data/rest/webmvc/config/RepositoryRestConfigurer.html
With Spring Boot you have to extends SpringBootRepositoryRestMvcConfiguration
if you use RepositoryRestMvcConfiguration the configuration define in application.properties may not worked
#Configuration
public class MyConfiguration extends SpringBootRepositoryRestMvcConfiguration {
#Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(Project.class);
}
}
But for a temporary need
You can use projection to include id in the serialization like :
#Projection(name = "allparam", types = { Person.class })
public interface ProjectionPerson {
Integer getIdPerson();
String getFirstName();
String getLastName();
}
Just add #JsonProperty annotation to the Id and it works.
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#JsonProperty
private long id;
another approach is to implement RepositoryRestConfigurerAdapter in configuration. (This approach will be usefull when you have to do marshalling in many places)
#Component
public class EntityExposingIdConfiguration extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
try {
Field exposeIdsFor = RepositoryRestConfiguration.class.getDeclaredField("exposeIdsFor");
exposeIdsFor.setAccessible(true);
ReflectionUtils.setField(exposeIdsFor, config, new ListAlwaysContains());
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
class ListAlwaysContains extends ArrayList {
#Override
public boolean contains(Object o) {
return true;
}
}
}
Hm, ok seems like I found the solution. Removing spring-boot-starter-data-rest from pom file and adding #JsonManagedReference to phoneNumbers and #JsonBackReference to person gives desired output. Json in response isn't pretty printed any more but now it has Id. Don't know what magic spring boot performs under hood with this dependency but I don't like it :)
Easy way: rename your variable private Long id; to private Long Id;
Works for me. You can read more about it here
Implement the RepositoryRestConfigurer and use #Configuration annotation on the class.
Here's the snippet
#Configuration
public class BasicConfig implements RepositoryRestConfigurer{
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
// TODO Auto-generated method stub
config.exposeIdsFor(Person.class);
}
}
You can also use the static configuration method to easily enable exposing ids in a few lines.
From the Spring Data Rest RepsositoryRestConfigurer docs:
static RepositoryRestConfigurer withConfig(Consumer<RepositoryRestConfiguration> consumer)
Convenience method to easily create simple RepositoryRestConfigurer instances that solely want to tweak the RepositoryRestConfiguration.
Parameters:
consumer - must not be null.
Since:
3.1
So this works for me in an existing #Configuration-annotated class:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
#Configuration
public class ApplicationConfiguration {
#Bean
public RepositoryRestConfigurer repositoryRestConfigurer() {
return RepositoryRestConfigurer.withConfig(repositoryRestConfiguration ->
repositoryRestConfiguration.exposeIdsFor(Person.class)
);
}
}

Mongo collection name with spring data not set correctly

I'm trying to use a base class for my mongo collections and then have the collection name come from the derived classes; however, the collection name is always just entity ( instead of, in this case, Derived).
I have the following abstract class:
public abstract class Entity {
#Id
private String id;
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
}
And derived classes like:
// I've also tried #TypeAlias("Derived")
#Document(collection = "Derived")
public class Derived extends Entity {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
With a repository like:
#Component
public interface Repository<T extends Entity> extends MongoRepository<T, String> {
T findById(String id);
}
Follow this working example.
Let's declare a base document model from which every document model must inherit
import java.math.BigInteger;
import org.springframework.data.annotation.Id;
public class BaseDocument {
#Id
private BigInteger id;
public BigInteger getId() {
return id;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (this.id == null || obj == null || !(this.getClass().equals(obj.getClass()))) {
return false;
}
BaseDocument that = (BaseDocument) obj;
return this.id.equals(that.getId());
}
#Override
public int hashCode() {
return id == null ? 0 : id.hashCode();
}
}
Just for example, I declare my Code model. Note that collection name, if differs from class name, can be set with #Document annotation
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
#Document(collection = "codes")
public class Code extends BaseDocument {
#Field("code")
private String code;
#Field("holiday")
private String holiday;
public Code(String code, String holiday) {
super();
this.code = code;
this.holiday = holiday;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getHoliday() {
return holiday;
}
public void setHoliday(String holiday) {
this.holiday = holiday;
}
#Override
public String toString() {
return "Code [code=" + code + ", holiday=" + holiday + "]";
}
}
At this point you can easily use these classes using MongoOperations and other classes in order to operate on database and collections. MongoOperations has all the methods you need to operate on collections such as: findOne, findAll and so on.
Just to be clear, you can use MongoOperations in the following way:
MongoOperations mongoOps = new MongoTemplate(new SimpleMongoDbFactory(new MongoClient(), "yourDBname"));
I strongly suggest to implement a service class which operate on collection, like this one, instead of operating directly on it:
import java.util.List;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
public class CodesService extends BaseService implements BaseOperations{
#Override
public BaseDocument findOne(Long id) {
Query query = new Query(Criteria.where("code").is("8"));
return this.mongoOps.findOne(query, Code.class);
}
#Override
public List<? extends BaseDocument> findAll() {
List<Code> subs = this.mongoOps.findAll(Code.class);
List<? extends BaseDocument> codes = subs;
return codes;
}
}
This class implements the following interface:
import java.util.List;
public interface BaseOperations {
public BaseDocument findOne(Long id);
public List<? extends BaseDocument> findAll();
}
Hope this may help.
Try to implement the Repository<T extends Entity> interface like given below
public interface SomeInterfaceName extends Repository<Derived>{
}
Now try to #Autowire the Repository<Derived> repo
It will give you the desired output

JSON Jackson, how to use read only property alias for backwards compatibilty?

We have a structure that represents configuration of some sort. We have had a typo in the word periodicity, it was wrongly spelled with 'o' as period*o*city. Below example source is the corrected one. However, I need to be able to read the old configuration files to maintain backwards compatibility.
Can I make JSON Jackson recognize the misspelled field/property on deserialization but ignore it on serialization?
We are using version 2.6.6 of JSON Jackson.
package foo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
#JsonInclude(Include.NON_EMPTY)
#JsonIgnoreProperties(ignoreUnknown = true)
public class Rule {
private LogPeriodicity periodicityLevel;
private Integer periodicity;
// ctors and some other methods omitted for brevity
public LogPeriodicity getPeriodicityLevel() {
return periodicityLevel;
}
public void setPeriodicityLevel(LogPeriodicity periodicityLevel) {
this.periodicityLevel = periodicityLevel;
}
public Integer getPeriodicity() {
return periodicity;
}
public void setPeriodicity(Integer periodicity) {
this.periodicity = periodicity;
}
}
If i got your question right you want something like this?
MyClass obj = mapper.readValue("{ \"name\" : \"value\"}", MyClass.class);
String serialized = mapper.writeValueAsString(obj);
MyClass obj2 = mapper.readValue("{ \"name2\" : \"value\"}", MyClass.class);
String serialized2 = mapper.writeValueAsString(obj2);
if( Objects.equals(serialized2, serialized))
System.out.println("Success " + serialized + " == " + serialized2 );
if you don't want extra field in POJO you can just add setter like this:
public static class MyClass {
#JsonProperty
private String name = null;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#JsonSetter
public void setName2(String name2) {
setName(name2);
}
}
You can probably also register legacy Mixin instead of #JsonSetter
public abstract class LegacyMyClassMixIn{
#JsonProperty("name")
private String name;
#JsonGetter("name")
public abstract String getName();
#JsonSetter("name2")
public abstract void setName(String name) ;
}
And use it like this:
SimpleModule module = new SimpleModule();
module.setMixInAnnotation(MyClass.class, LegacyMyClassMixIn.class);
mapper2.registerModule(module);
Btw in Gson it can be done with just 1 line #SerializedName(value="name", alternate={"name2"}) public String name = null;

Categories

Resources