Spring service method and a complex validation logic/rules - java

At my Spring/Boot Java project I have a set of service methods, for example like a following one:
#Override
public Decision create(String name, String description, String url, String imageUrl, Decision parentDecision, Tenant tenant, User user) {
name = StringUtils.trimMultipleSpaces(name);
if (org.apache.commons.lang3.StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Decision name can't be blank");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(url) && !urlValidator.isValid(url)) {
throw new IllegalArgumentException("Decision url is not valid");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(imageUrl) && !urlValidator.isValid(imageUrl)) {
throw new IllegalArgumentException("Decision imageUrl is not valid");
}
if (user == null) {
throw new IllegalArgumentException("User can't be empty");
}
if (tenant != null) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(user.getId());
if (!userTenants.contains(tenant)) {
throw new IllegalArgumentException("User doesn't belong to this tenant");
}
}
if (parentDecision != null) {
if (tenant == null) {
if (findFreeChildDecisionByName(parentDecision.getId(), name) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
} else {
if (findTenantedChildDecisionByName(parentDecision.getId(), name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
}
Tenant parentDecisionTenant = tenantDao.findTenantForDecision(parentDecision.getId());
if (parentDecisionTenant != null) {
if (tenant == null) {
throw new IllegalArgumentException("Public decision cannot be added as a child to tenanted parent decision");
}
if (!parentDecisionTenant.equals(tenant)) {
throw new IllegalArgumentException("Decision cannot belong to tenant other than parent decision tenant");
}
} else {
if (tenant != null) {
throw new IllegalArgumentException("Tenanted decision cannot be added as a child to public parent decision");
}
}
} else {
if (tenant == null) {
if (findFreeRootDecisionByName(name) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name already exists");
}
} else {
if (findTenantedRootDecisionByName(name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name for this tenant already exists");
}
}
}
Decision decision = createOrUpdate(new Decision(name, description, url, imageUrl, parentDecision, user, tenant));
if (parentDecision != null) {
parentDecision.addChildDecision(decision);
}
criterionGroupDao.create(CriterionGroupDaoImpl.DEFAULT_CRITERION_GROUP_NAME, null, decision, user);
characteristicGroupDao.create(CharacteristicGroupDaoImpl.DEFAULT_CHARACTERISTIC_GROUP_NAME, null, decision, user);
return decision;
}
As you can see, most of the code lines from this method are occupied with a validation logic and I continue to adding a new validation cases there.
I want to refactor this method and move validation logic outside of this method in a more appropriate places. Please suggest how it can be done with Spring framework.

As chrylis mentioned in the comments, you can achieve this goal by using JSR-303 bean validation. The first step is to create a class that contains your input parameters:
public class DecisionInput {
private String name;
private String description;
private String url;
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
private User user;
// Constructors, getters, setters, ...
}
After that, you can start adding validation annotations, for example:
public class DecisionInput {
#NotEmpty
private String name;
#NotEmpty
private String description;
#NotEmpty
private String url;
#NotEmpty
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
#NotNull
private User user;
// Constructors, getters, setters, ...
}
Be aware that the #NotEmpty annotation is not a standard JSR-303 annotation, but a Hibernate annotation. If you prefer using standard JSR-303 you can always create your own custom validator. For your tenant and your decision, you certainly need a custom validator. First of all create an annotation (eg #ValidTenant). On your annotation class, make sure to add the #Constraint annotation, for example:
#Constraint(validatedBy = TenantValidator.class) // Your validator class
#Target({ TYPE, ANNOTATION_TYPE }) // Static import from ElementType, change this to METHOD/FIELD if you want to create a validator for a single field (rather than a cross-field validation)
#Retention(RUNTIME) // Static import from RetentionPolicy
#Documented
public #interface ValidTenant {
String message() default "{ValidTenant.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Now you have to create the TenantValidator class and make it implement ConstraintValidator<ValidTenant, DecisionInput>, for example:
#Component
public class TenantValidator implements ConstraintValidator<ValidTenant, DecisionInput> {
#Autowired
private TenantDAO tenantDao;
#Override
public void initialize(ValidTenant annotation) {
}
#Override
public boolean isValid(DecisionInput input, ConstraintValidatorContext context) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(input.getUser().getId());
return userTenants.contains(input.getTenant());
}
}
The same can be done for the validation of the parent decision. Now you can just refactor your service method to this:
public Decision create(#Valid DecisionInput input) {
// No more validation logic necessary
}
If you want to use your own error messages, I suggest reading this answer. Basically you create a ValidationMessages.properties file and put your messages there.

Related

Is there a way to Spring custom validator reject value without field having a getter method?

I'm currently studying building API's with Spring. I'm working with Spring Validator to validate my input. Here it is my custom validator:
public class NewHoldValidator implements Validator {
private EntityManager manager;
public NewHoldValidator(EntityManager manager) {
this.manager = manager;
}
#Override
public boolean supports(Class<?> clazz) {
return NewHoldRequest.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
if (errors.hasErrors()) {
return;
}
NewHoldRequest request = (NewHoldRequest) target;
Patron patron = manager.find(Patron.class, request.patronId);
BookInstance bookInstance = manager.find(BookInstance.class, request.bookInstanceId);
Assert.state(patron != null, "Patron does not exists.");
Assert.state(bookInstance != null, "Book instance does not exists.");
if (!bookInstance.acceptToBeHoldTo(patron)) {
errors.reject(null, "This book instance cannot be hold to this patron");
}
if (!request.hasDaysHold()) {
if (!patron.researcher()) {
errors.rejectValue("daysHold", null, "You need to pass a daysHold attribute");
}
}
}
}
And here is my NewHoldRequest class:
public class NewHoldRequest {
#NotNull
public final Long patronId;
#NotNull
public final Long bookInstanceId;
#Positive
#Max(60)
public final Integer daysHold;
public NewHoldRequest(#NotNull Long patronId, #NotNull Long bookInstanceId, #Positive #Max(60) Integer daysHold) {
this.patronId = patronId;
this.bookInstanceId = bookInstanceId;
this.daysHold = daysHold;
}
#Override
public String toString() {
return "NewHoldRequest{" + "patronId=" + patronId + ", bookId=" + bookInstanceId + ", daysHold=" + daysHold + '}';
}
public boolean hasDaysHold() {
return this.daysHold != null;
}
Even if my field "daysHold" is public I still need to create a getter to it so Spring can show the rejected error properly, otherwise, it will throw a NotReadablePropertyException. Is there a way to define that Spring can reject public fields without getters or I will need to add accessor methods to all fields I want to reject?
Here is the message that shows up when my validation is triggered.
org.springframework.beans.NotReadablePropertyException: Invalid property 'daysHold' of bean class [com.api.library.usecases.newhold.NewHoldRequest]: Bean property 'daysHold' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
I'll advise you to use the interface ConstraintValidator. It's a generic's interface, without casting and other problems.
You should create a custom constraint annotation for you it's #NewHoldValid after that implement CustomValidator interface:
#Component
public class NewHoldValidator implements ConstraintValidator<NewHoldValid, NewHoldRequest> {
#Override
public boolean isValid(NewHoldRequest value, ConstraintValidatorContext context) {
if(ANY_CONDITION){
return true;
}else {
return false;
}
}
After that use #NewHoldValid annotation above your NewHoldRequest.
If you'll want to set an error message text which different from default use that:
context.buildConstraintViolationWithTemplate("Your error message").addConstraintViolation();

#Notnull Spring Custom Annotation Validation with multiple field

How to write (type level annotation) custom annotation to allow a choice validation (exactly one of a number of properties has to be not null)?
Even if this "question" is very broad I will give an answer as I have exactly the needed piece of code here:
#Target(ElementType.TYPE)
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = ChoiceValidator.class)
public #interface Choice {
String[] value();
boolean multiple() default false;
String message() default "{com.stackoverflow.validation.Choice.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Here a validator using a bean property access:
public class ChoiceValidator implements ConstraintValidator<Choice, Object> {
private String[] properties;
private boolean allowMultiple;
#Override
public void initialize(Choice constraintAnnotation) {
if (constraintAnnotation.value().length < 2) {
throw new IllegalArgumentException("at least two properties needed to make a choice");
}
properties = constraintAnnotation.value();
allowMultiple = constraintAnnotation.multiple();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
BeanInfo info = getBeanInfo(value.getClass());
long notNull = Stream.of(properties)
.map(property -> Stream.of(info.getPropertyDescriptors())
.filter(desr -> desr.getName().equals(property))
.findAny()
.orElse(null)
)
.map(prop -> getProperty(prop, value))
.filter(Objects::nonNull)
.count();
return allowMultiple ? notNull != 0 : notNull == 1;
} catch (IntrospectionException noBean) {
return false;
}
}
private Object getProperty(PropertyDescriptor prop, Object bean) {
try {
return prop.getReadMethod() == null ? null : prop.getReadMethod().invoke(bean);
} catch (ReflectiveOperationException noAccess) {
return null;
}
}
}
A typical usage can look like this (lombok annotation to generate getters and setters):
#Data
#Choice({"one", "two"})
class OneOf {
private String one;
private String two;
private String whatever;
}
#Data
#Choice(value = {"one", "two"}, multiple = true)
class AnyOf {
private String one;
private String two;
}
But to clarify: Stackoverflow is a QA community for developers to exchange knowledge. It is not a place to ask "Can you please code this for me for free?". All valuable contributors at least try to solve problems first and come with detailed questions afterwards. People answering questions spend their spare time and are not payed for. Please show respect by asking for specific problems and showing own efforts in the future.

How to create custom Validation Messages based on an annotation property?

I'm using the Hibernate #NotNull validator, and I'm trying to create custom messages to tell the user which field has generated the error when it is null. Something like this:
notNull.custom = The field {0} can't be null.
(this will be in my ValidationMessages.properties file).
Where the {0} should be the field name passed to the validator this way:
#NotNull(field="field name")
There is any way I can do that?
To customize your annotation message you need to disable the existing violation message inside isValid() method and build a new violation message and add it.
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
In the given below example, I am creating an annotation for input date validation on the basis of "invalid date", "can't be greater than today date" and "date format is correct or not".
#CheckDateIsValid(displayPattern = "DD/MM/YYYY", programPattern = "dd/MM/yyyy", groups = Order2.class)
private String fromDate;
Annotation Interface -
public #interface CheckDateIsValid {
String message() default "default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String displayPattern();
String programPattern();
}
Annotation implementation Class -
public class CheckDateIsValidValidator implements ConstraintValidator<CheckDateIsValid, String> {
#Value("${app.country.timeZone}")
private String timeZone;
private String displayPattern;
private String programPattern;
#Override
public void initialize(CheckDateIsValid constraintAnnotation) {
this.displayPattern = constraintAnnotation.displayPattern();
this.programPattern = constraintAnnotation.programPattern();
}
#Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
try {
// disable existing violation message
constraintContext.disableDefaultConstraintViolation();
if (object == null) {
return true;
}
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(programPattern);
LocalDateTime time = LocalDate.parse(object, formatter).atStartOfDay();
ZoneOffset zoneOffSet = ZoneOffset.of(timeZone);
OffsetDateTime todayDateTime = OffsetDateTime.now(zoneOffSet);
if (time == null) {
customMessageForValidation(constraintContext, "date is not valid");
return false;
} else if (todayDateTime.isBefore(time.atOffset(zoneOffSet))) {
customMessageForValidation(constraintContext, "can't be greater than today date");
return false;
}
return time != null;
} catch (Exception e) {
customMessageForValidation(constraintContext, "date format should be like " + displayPattern);
return false;
}
}
private void customMessageForValidation(ConstraintValidatorContext constraintContext, String message) {
// build new violation message and add it
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
}
}
If your requirement can be satisfied by interpolating hibernate messages, so you can create/name your *property file like that:
ValidationMessages.properties
And inside that:
javax.validation.constraints.NotNull.message = CUSTOMIZED MESSAGE WHEN NOTNULL is violated!
Hibernate by default searches a ResourceBundle named ValidationMessages. Also a locale might be involved: ValidationMessages_en, ValidationMessages_de, <..>
Hibernate will provide your customized message through interpolatedMessage parameter, so all ConstraintViolationException relevant information (included your message ) will be showed. So you message will be a part of real exception. Some unwieldy information will be provided!
If you want to make your custom exception (without default ConstraintViolationException behavior) check this out:
Using GenericDao concept, consider the following
public void saveOrUpdate(IEntity<?> entity) {
try {
if(entity.getId == null) {
em.persist(entity);
} else {
em.merge(entity)l
}
} catch(ConstraintViolationException cve) {
throw new ConstraintViolationEx(constructViolationMessage(cve.getConstraintViolations()));
}
}
private String constructMessage(Set<ConstraintViolation<?>> pConstraintViolations) {
StringBuilder customMessages = new StringBuilder();
for(ConstraintViolation<?> violation : pConstraintViolations) {
String targetAnnotation = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
if(supportsCustomMessage(targetAnnotation)) {
applyMessage(violation, targetAnnotation, customMessages);
} else {
// do something with not customized constraints' messages e.g. append it to existing container
}
}
return customMessages.toString();
}
private void applyMessage(ConstraintViolation<?> pViolation, String pTargetAnnotation, StringBuilder pCustomMessages) {
String targetClass = pViolation.getRootBean().getClass().getName();
String targetField = pViolation.getPropertyPath().toString();
pCustomMessages.append(MessageFormat.format(getMessageByAnnotation(pTargetAnnotation), targetClass, targetField));
pCustomMessages.append(System.getProperty("line.separator"));
}
private String getBundleKey() {
return "ValidationMessages"; //FIXME: hardcoded - implement your true key
}
private String getMessageByAnnotation(String pTargetAnnotation) {
ResourceBundle messages = ResourceBundle.getBundle(getBundleKey());
switch(pTargetAnnotation) {
case "NotNull":
return messages.getString(pTargetAnnotation + ".message");
default:
return "";
}
}
private boolean supportsCustomMessage(String pTargetAnnotation) {
return customizedConstraintsTypes.contains(pTargetAnnotation);
}
Produced result:
test.model.exceptions.ConstraintViolationEx
test.model.Person : name cannot be null
test.model.Person : surname cannot be null
A hibernate ConstraintViolation provides relevant information about root class and restricted field. As you see, it applies for all hibernate supported constraints, so you need to check if current annotation can be customized by supportsCustomMessage(<..>)! If it can (it's up to you), you should get appropriate message by constraint annotation doing `getMessageByAnnotation(<..>)'.
All you need to do is implement not supported constraints logic. For example it can append it's cause message or interpolated with default message (and true exception goes to *log file)

Annotation reflection (using getAnnotation) does not work

I have to following code to check whether the entity in my model has a nullable=false or similar annotation on a field.
import javax.persistence.Column;
import .....
private boolean isRequired(Item item, Object propertyId) {
Class<?> property = getPropertyClass(item, propertyId);
final JoinColumn joinAnnotation = property.getAnnotation(JoinColumn.class);
if (null != joinAnnotation) {
return !joinAnnotation.nullable();
}
final Column columnAnnotation = property.getAnnotation(Column.class);
if (null != columnAnnotation) {
return !columnAnnotation.nullable();
}
....
return false;
}
Here's a snippet from my model.
import javax.persistence.*;
import .....
#Entity
#Table(name="m_contact_details")
public class MContactDetail extends AbstractMasterEntity implements Serializable {
#Column(length=60, nullable=false)
private String address1;
For those people unfamiliar with the #Column annotation, here's the header:
#Target({METHOD, FIELD})
#Retention(RUNTIME)
public #interface Column {
I'd expect the isRequired to return true every now and again, but instead it never does.
I've already done a mvn clean and mvn install on my project, but that does not help.
Q1: What am I doing wrong?
Q2: is there a cleaner way to code isRequired (perhaps making better use of generics)?
property represents a class (it's a Class<?>)
#Column and #JoinColumn can only annotate fields/methods.
Consequently you will never find these annotations on property.
A slightly modified version of your code that prints out whether the email property of the Employee entity is required:
public static void main(String[] args) throws NoSuchFieldException {
System.out.println(isRequired(Employee.class, "email"));
}
private static boolean isRequired(Class<?> entity, String propertyName) throws NoSuchFieldException {
Field property = entity.getDeclaredField(propertyName);
final JoinColumn joinAnnotation = property.getAnnotation(JoinColumn.class);
if (null != joinAnnotation) {
return !joinAnnotation.nullable();
}
final Column columnAnnotation = property.getAnnotation(Column.class);
if (null != columnAnnotation) {
return !columnAnnotation.nullable();
}
return false;
}
Note that this is a half-baked solution, because JPA annotations can either be on a field or on a method. Also be aware of the difference between the reflection methods like getFiled()/getDeclaredField(). The former returns inherited fields too, while the latter returns only fields of the specific class ignoring what's inherited from its parents.
The following code works:
#SuppressWarnings("rawtypes")
private boolean isRequired(BeanItem item, Object propertyId) throws SecurityException {
String fieldname = propertyId.toString();
try {
java.lang.reflect.Field field = item.getBean().getClass().getDeclaredField(fieldname);
final JoinColumn joinAnnotation = field.getAnnotation(JoinColumn.class);
if (null != joinAnnotation) {
return !joinAnnotation.nullable();
}
final Column columnAnnotation = field.getAnnotation(Column.class);
if (null != columnAnnotation) {
return !columnAnnotation.nullable();
}
} catch (NoSuchFieldException e) {
//not a problem no need to log this event.
return false;
}
}

Trimming Struts2 textfield string input

What is the best way to trim this string/where is the best place to put the trim code?
Say I have the following textfield in my jsp:
<s:textfield label="First Name" name="person.firstname"/>
The action class:
public class BaseAction extends ActionSupport implements ServletRequestAware, SessionAware {
private Person person;
// Getters, setters and action logic
}
The bean:
public class Person implements Serializable {
private String lastname;
private String firstname;
// Getters and setters
}
I can change the default setting in the bean but this seems like a hack:
public void setFirstname(String firstname) {
this.firstname = firstname.trim();
}
EDIT: I did also see this question: struts2 trim all string obtained from forms where it's also suggested by some that the "correct" method is to use an interceptor.
Why is an interceptor the "correct" way? What is so wrong about changing the bean's setters?
It can be done with Struts2 converters.
public class TrimmingStringConverter extends StrutsTypeConverter {
public Object convertFromString(Map ctx, String[] values, Class arg2) {
if (values != null && values.length > 0) {
return values[0].trim();
}
return null;
}
public String convertToString(Map ctx, Object o) {
if (o != null) {
return o.toString();
}
else {
return null;
}
}
public Object convertValue(Map context, Object o, Class toClass)
{
if (o == null) {
return null;
} else if (toClass == java.lang.String.class) {
if (o instanceof String[]) {
String[] os = (String[]) o;
if (os.length > 0) {
return os[0].trim();
}
}
return o.toString().trim();
}
return super.convertValue(context, o, toClass);
}
}
It must be registered in xwork-conversion.properties:
java.lang.String=es.jogaco.webapp.TrimmingStringConverter
This will be applied to all user input.
It will work if you have the default struts2 interceptors. Quoted from struts2 doc:
By default, the conversion interceptor is included in struts-default.xml in the default stack
Plus I have it working in my struts2 app.
Short answer is Not by default, there is no build in mechanism to do this and you either need to do it in your action class or some-kind of java-script will do that for you.
Other possible way is to create an interceptor to do this with option to excludes or something like on similar trek.
I believe Interceptor is a good way to do this,its better to have such interceptor comes with S2.

Categories

Resources