I am trying to set up a translation service which will use gnu gettext. The basic idea is from: https://thedarkgod.wordpress.com/2009/01/18/java-webapp-localization-through-gettext/
But I would like it to be implemented as a service. For some odd reasons, I would like to have this class:
import webapp.service.TranslationService;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* AppStrings<p>
* <p/>
* DOC-TODO:
*/
#Service("applicationStrings")
public class ApplicationStrings {
#Autowired private TranslationService translationService;
public String CART_SUBTYPE = "Cart";
public ApplicationStrings(){
Locale locale = LocaleContextHolder.getLocale();
//translationService.initLocale();
this.updateLocale();
}
public void updateLocale(){
Locale locale = LocaleContextHolder.getLocale();
translationService.updateLocale(locale);
this.setLocale(locale);
}
public void setLocale(Locale locale){
//this.CART_SUBTYPE = translationService._("Cart");
this.CART_SUBTYPE = "CART_DEF - Check ApplicationStrings";
}
}
Part of the code is commented as it doesn't really work... but it might reveal my target. The indeed problematic service class looks like this:
package webapp.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.gettext.GettextResource;
import java.util.ResourceBundle;
import java.util.Locale;
import java.text.MessageFormat;
import java.util.Hashtable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
/**
* From: https://thedarkgod.wordpress.com/2009/01/18/java-webapp-localization-through-gettext/
*
*/
#Service("translationService")
public class TranslationService {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static Hashtable<Locale, ResourceBundle> trht = new Hashtable<Locale, ResourceBundle> ();
private ResourceBundle myResources = null;
public TranslationService () {
Locale locale = LocaleContextHolder.getLocale();
if ( locale == null){
logger.warn("Setting a default locale!");
locale = Locale.ENGLISH;
}
this.updateLocale(locale);
}
public TranslationService (Locale locale) {
this.updateLocale(locale);
}
public void initLocale () {
Locale locale = LocaleContextHolder.getLocale();
this.updateLocale(locale);
}
public void updateLocale(Locale locale)
{
synchronized (trht)
{
if (!trht.contains (locale))
{
try
{
myResources = GettextResource.getBundle("translation", locale);
}
catch (Exception e)
{
logger.error("UPDATE: Exception.");
e.printStackTrace();
}
trht.put ((Locale) locale.clone(), myResources);
}
else
myResources = trht.get (locale);
}
}
public String _(String s)
{
if (myResources == null) return s;
return GettextResource.gettext (myResources, s);
}
public String N_(String singular, String plural, long n)
{
if (myResources == null) return (n == 1 ? singular : plural);
return GettextResource.ngettext (myResources, singular,
plural, n);
}
public String format (String s, Object ... args)
{
return MessageFormat.format (_(s), args);
}
public String formatN (String singular, String plural,
long n, Object ... args)
{
return MessageFormat.format (N_(singular, plural, n), args);
}
}
These classes are not even a bit used in my application, but of course spring will instantiate them and the error looks like this:
java.lang.NullPointerException
at webapp.constant.ApplicationStrings.updateLocale(ApplicationStrings.java:34)
at webapp.constant.ApplicationStrings.<init>(ApplicationStrings.java:29)
Please note that all other #service are working so I assume it is not a problem of some xml configuration, for which I found few questions (and answers) on stackoverflow, again other services are working so the configuration should be fine.
I assume it is my newbie approach which might miss some simple keyword.. or even concept..
Thanks and cheers
Together with the answers, as this might be a general approach to gettext and indeed related to the question, I would appreciate a couple of comments on the actual approach.
Also I am totally not sure about the "sychronized" part: can this be the problem?
You are calling dependencies in your constructor. Spring wires the bean dependencies after constructing the object so here you are trying to call those methods a bit too early, Spring had no chance to wire your bean at that moment. To solve the issue you could make use of the #PostConstruct annotation. A method marked with this is always called by Spring after constructing and wiring up a bean.
e.g.
public ApplicationStrings() {
}
#PostConstruct
public void init() {
//translationService.initLocale();
this.updateLocale();
}
Related
I have a POJO:
class Test {
private int i;
void setI(int i) {
this.i = i;
}
}
this is what I have so far for the aspect:
public aspect t perthis(within(#Tracking *)){
private Set<String> set = new HashSet<>();
pointcut setterMethod() : execution(public void set*(..));
after(Object o) returning() : setterMethod() && this(o) {
set.add(thisJoinPoint.getSignature().getName());
System.out.println(set);
}
public Set<String> go() {
return set;
}
}
I want a Set<String> set for any instance of ANY class that has #Tracking. I also want to add the go() method for any instance of ANY class that has #Tracking.
Can't figure out the syntax. The go() method doesn't get added. If I put Test.go() then the method get added, but then it crashes during runtime.
Marker interface:
package de.scrum_master.tracking;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Retention(RUNTIME)
#Target(TYPE)
public #interface Track {}
Sample POJOs with/without marker annotation:
We use two sample classes for positive/negative tests.
package de.scrum_master.app;
import de.scrum_master.tracking.Track;
#Track
class TestTracked {
private int number;
private String text;
public void setNumber(int number) {
this.number = number;
}
public void setText(String text) {
this.text = text;
}
#Override
public String toString() {
return "TestTracked [number=" + number + ", text=" + text + "]";
}
}
package de.scrum_master.app;
class TestUntracked {
private int number;
private String text;
public void setNumber(int number) {
this.number = number;
}
public void setText(String text) {
this.text = text;
}
#Override
public String toString() {
return "TestUntracked [number=" + number + ", text=" + text + "]";
}
}
"Dirty tracker" interface:
We want the aspect to implement the following interface for each class annotated by #Track by means of inter-type declaration (ITD).
package de.scrum_master.tracking;
import java.util.Set;
public interface Trackable {
Set<String> getDirty();
}
Driver application:
Here, we are assuming that each POJO class annotated with the marker interface automagically implements the Trackable interface and therefore knows the getDirty() method, which the we call in order to verify that the aspect correctly tracks setter calls.
package de.scrum_master.app;
import de.scrum_master.tracking.Trackable;
public class Application {
public static void main(String[] args) {
TestTracked testTracked = new TestTracked();
testTracked.setNumber(11);
testTracked.setText("foo");
System.out.println(testTracked);
if (testTracked instanceof Trackable)
System.out.println("Dirty members: " + ((Trackable) testTracked).getDirty());
TestUntracked testUntracked = new TestUntracked();
testUntracked.setNumber(22);
testUntracked.setText("bar");
System.out.println(testUntracked);
if (testUntracked instanceof Trackable)
System.out.println("Dirty members: " + ((Trackable) testUntracked).getDirty());
}
}
Aspect:
This aspect makes each #Track-annotated class implement interface Trackable and provides both a private field storing tracking information and a getDirty() method implementation returning its value. Furthermore, the aspect makes sure to store the "dirty" information for each successfully executed setter.
package de.scrum_master.tracking;
import java.util.HashSet;
import java.util.Set;
public aspect TrackingAspect {
private Set<String> Trackable.dirty = new HashSet<>();
public Set<String> Trackable.getDirty() {
return dirty;
}
declare parents : #Track * implements Trackable;
pointcut setterMethod() : execution(public void set*(..));
after(Trackable trackable) returning() : setterMethod() && this(trackable) {
System.out.println(thisJoinPoint);
trackable.dirty.add(thisJoinPoint.getSignature().getName().substring(3));
}
}
Console log:
You will see this when running the driver application:
execution(void de.scrum_master.app.TestTracked.setNumber(int))
execution(void de.scrum_master.app.TestTracked.setText(String))
TestTracked [number=11, text=foo]
Dirty members: [Number, Text]
TestUntracked [number=22, text=bar]
The reason why we do not need perthis or pertarget instantiation is that we store the "dirty" information right inside the original classes. Alternatively, we could use per* instantiation and keep all information inside the corresponding aspect instances instead of using ITD. In that case however, the "dirty" information would be unaccessible from outside the aspect, which might even be desirable with regard to encapsulation. But then, whatever action needs to be performed when storing the "dirty" instances, would also need to happen from inside the aspect. As you did not provide an MCVE and hence your question is lacking detail, I did not consider this functionality in the aspect. I can easily imagine how this could be done with both aspect variants - per* instantiation vs. ITD - but I hate to speculate and then be wrong.
I have a spring boot app, and I want to send DTO validation constraints as well as field value to the client.
Having DTO
class PetDTO {
#Length(min=5, max=15)
String name;
}
where name happens to be 'Leviathan', should result in this JSON being sent to client:
{
name: 'Leviathan'
name_constraint: { type: 'length', min:5, max: 15},
}
Reasoning is to have single source of truth for validations. Can this be done with reasonable amount of work?
To extend Frederik's answer I'll show a little sample code that convers an object to map and serializes it.
So here is the User pojo:
import org.hibernate.validator.constraints.Length;
public class User {
private String name;
public User(String name) {
this.name = name;
}
#Length(min = 5, max = 15)
public String getName() {
return name;
}
}
Then the actual serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
public class UserSerializer extends StdSerializer<User> {
public UserSerializer(){
this(User.class);
}
private UserSerializer(Class t) {
super(t);
}
#Override
public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> properties = beanProperties(bean);
gen.writeStartObject();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gen.writeObjectField(entry.getKey(), entry.getValue());
}
gen.writeEndObject();
}
private static Map<String, Object> beanProperties(Object bean) {
try {
return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
.filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
.flatMap(descriptor -> {
String name = descriptor.getName();
Method getter = descriptor.getReadMethod();
Object value = ReflectionUtils.invokeMethod(getter, bean);
Property originalProperty = new Property(name, value);
Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
.map(anno -> new Property(name + "_constraint", annotationProperties(anno)));
return Stream.concat(Stream.of(originalProperty), constraintProperties);
})
.collect(toMap(Property::getName, Property::getValue));
} catch (Exception e) {
return Collections.emptyMap();
}
}
// Methods from Annotation.class
private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");
private static Map<String, Object> annotationProperties(Annotation anno) {
try {
Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
.filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
.map(descriptor -> {
String name = descriptor.getName();
Method method = descriptor.getMethod();
Object value = ReflectionUtils.invokeMethod(method, anno);
return new Property(name, value);
});
Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
} catch (IntrospectionException e) {
return Collections.emptyMap();
}
}
private static class Property {
private String name;
private Object value;
public Property(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
}
And finally we need to register this serializer to be used by Jackson:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {
#Bean
public Jackson2ObjectMapperBuilder mapperBuilder(){
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.serializers(new UserSerializer());
return jackson2ObjectMapperBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SerializationApp.class, args);
}
}
#RestController
class SerializationController {
#GetMapping("/user")
public User user() {
return new User("sample");
}
}
The Json that will be emitted:
{
"name_constraint":{
"min":5,
"max":15,
"payload":[],
"groups":[],
"message":"{org.hibernate.validator.constraints.Length.message}",
"type":"org.hibernate.validator.constraints.Length"
},
"name":"sample"
}
Hope this helps. Good luck.
You can always use a custom Jackson Serializer for this. Plenty of docs to do this can be found on the internet, might look something like this:
public void serialize(PetDTO value, JsonGenerator jgen, ...) {
jgen.writeStartObject();
jgen.writeNumberField("name", value.name);
jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}
public ConstaintDTO getConstraintValue(PetDTO value) {
// Use reflection to check if the name field on the PetDTO is annotated
// and extract the min, max and type values from the annotation
return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}
You may want to create a base-DTO class for which the converter kicks in so you don't have to create a custom converter for all your domain objects that need to expose the constraints.
By combining reflection and smart use of writing fields, you can get close. Downside is you can't take advantage of the #JsonXXX annotations on your domain objects, since you're writing the JSON yourself.
More ideal solution whould be to have Jackson convert, but have some kind of post-conversion-call to add additional XX_condtion properties to the object. Maybe start by overriding the default object-serializer (if possible)?
Feign default expander to convert param:
final class ToStringExpander implements Expander {
#Override
public String expand(Object value) {
return value.toString();
}
}
I want custom it to convert user to support GET param, like this
#FeignClient("xx")
interface UserService{
#RequestMapping(value="/users",method=GET)
public List<User> findBy(#ModelAttribute User user);
}
userService.findBy(user);
What can i do?
First,you must write a expander like ToJsonExpander:
public class ToJsonExpander implements Param.Expander {
private static ObjectMapper objectMapper = new ObjectMapper();
public String expand(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new ExpanderException(e);
}
}
}
Second, write a AnnotatedParameterProcessor like JsonArgumentParameterProcessor to add expander for your processor.
public class JsonArgumentParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<JsonArgument> ANNOTATION = JsonArgument.class;
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
MethodMetadata data = context.getMethodMetadata();
String name = ANNOTATION.cast(annotation).value();
String method = data.template().method();
Util.checkState(Util.emptyToNull(name) != null,
"JsonArgument.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
if (method != null && (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method))) {
data.formParams().add(name);
} else {
`data.indexToExpanderClass().put(context.getParameterIndex(), ToJsonExpander.class);`
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
data.template().query(name, query);
}
return true;
}
}
Third,add it to Feign configuration.
#Bean
public Contract feignContract(){
List<AnnotatedParameterProcessor> processors = new ArrayList<>();
processors.add(new JsonArgumentParameterProcessor());
processors.add(new PathVariableParameterProcessor());
processors.add(new RequestHeaderParameterProcessor());
processors.add(new RequestParamParameterProcessor());
return new SpringMvcContract(processors);
}
Now, you can use #JsonArgument to send model argument like:
public void saveV10(#JsonArgument("session") Session session);
I don't know what #ModelAttribute does but I was looking for a way to convert #RequestParam values so I did this:
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;
import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164;
#Component
public class PhoneNumberFeignFormatterRegistrar implements FeignFormatterRegistrar {
private final PhoneNumberUtil phoneNumberUtil;
public PhoneNumberFeignFormatterRegistrar(PhoneNumberUtil phoneNumberUtil) {
this.phoneNumberUtil = phoneNumberUtil;
}
#Override
public void registerFormatters(FormatterRegistry registry) {
registry.addConverter(Phonenumber.PhoneNumber.class, String.class, source -> phoneNumberUtil.format(source, E164));
}
}
Now stuff like the following works
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
#FeignClient("data-service")
public interface DataClient {
#RequestMapping(method = RequestMethod.GET, value = "/phoneNumbers/search/findByPhoneNumber")
Resource<PhoneNumberRecord> getPhoneNumber(#RequestParam("phoneNumber") Phonenumber.PhoneNumber phoneNumber);
}
As the open feign issue and spring doc say:
The OpenFeign #QueryMap annotation provides support for POJOs to be used as GET parameter maps.
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map since 2.1.0.
You can use it like this:
#GetMapping("user")
String getUser(#SpringQueryMap User user);
public class User {
private String name;
private int age;
...
}
I have an entity named EmployeeDepartment as below
#IdClass(EmployeeDepartmentPK.class) //EmployeeDepartmentPK is a serializeable object
#Entity
EmployeeDepartment{
#Id
private String employeeID;
#Id
private String departmentCode;
---- Getters, Setters and other props/columns
}
and I have a Spring Data Repository defined as as below
#RepositoryRestResource(....)
public interface IEmployeeDepartmentRepository extends PagingAndSortingRepository<EmployeeDepartment, EmployeeDepartmentPK> {
}
Further, I have a converter registered to convert from String to EmployeeDepartmentPK.
Now, for an entity, qualified by ID employeeID="abc123" and departmentCode="JBG", I expect the ID to use when SDR interface is called is abc123_JBG.
For example http://localhost/EmployeeDepartment/abc123_JBG should fetch me the result and indeed it does.
But, when I try to save an entity using PUT, the ID property available in BasicPersistentEntity class of Spring Data Commons is having a value of
abc123_JBG for departmentCode. This is wrong. I'm not sure if this is an expected behaviour.
Please help.
Thanks!
Currently Spring Data REST only supports compound keys that are represented as by a single field. That effectively means only #EmbeddedId is supported. I've filed DATAJPA-770 to fix that.
If you can switch to #EmbeddedId you still need to teach Spring Data REST the way you'd like to represent your complex identifier in the URI and how to transform the path segment back into an instance of your id type. To achieve that, implement a BackendIdConverter and register it as Spring bean.
#Component
class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
// Make sure you validate the input
String[] parts = id.split("_");
return new YourEmbeddedIdType(parts[0], parts[1]);
}
#Override
public String toRequestId(Serializable source, Class<?> entityType) {
YourIdType id = (YourIdType) source;
return String.format("%s_%s", …);
}
#Override
public boolean supports(Class<?> type) {
return YourDomainType.class.equals(type);
}
}
If you can't use #EmbeddedId, you can still use #IdClass. For that, you need the BackendIdConverter as Oliver Gierke answered, but you also need to add a Lookup for your domain type:
#Configuration
public class IdClassAllowingConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.withEntityLookup().forRepository(EmployeeDepartmentRepository.class, (EmployeeDepartment ed) -> {
EmployeeDepartmentPK pk = new EmployeeDepartmentPK();
pk.setDepartmentId(ed.getDepartmentId());
pk.setEmployeeId(ed.getEmployeeId());
return pk;
}, EmployeeDepartmentRepository::findOne);
}
}
Use #BasePathAwareController to customize Spring data rest controller.
#BasePathAwareController
public class CustInfoCustAcctController {
#Autowired
CustInfoCustAcctRepository cicaRepo;
#RequestMapping(value = "/custInfoCustAccts/{id}", method = RequestMethod.GET)
public #ResponseBody custInfoCustAccts getOne(#PathVariable("id") String id) {
String[] parts = id.split("_");
CustInfoCustAcctKey key = new CustInfoCustAcctKey(parts[0],parts[1]);
return cicaRepo.getOne(key);
}
}
It's work fine for me with sample uri /api/custInfoCustAccts/89232_70
A more generic approach would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}
I want to manually validate (not using #Valid or #Validated) using groups and return a BindingResult.
I have a spring validator configured
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" >
<property name="validationMessageSource" ref="messageSource"/>
</bean>
And this is the class that does the validation, it works but notice that groups is not being used.
#Component
public class ObjectValidatorImpl implements ObjectValidator {
private final Validator validator;
#Autowired
public ObjectValidatorImpl(final Validator validator) {
super();
this.validator = validator;
}
#Override
public final <T> BindingResult getBindingResults(
final T objectToValidate, final Class<?>...groups) {
final DataBinder binder = new DataBinder(objectToValidate);
binder.setValidator(validator);
//ideally, I would like to set the groups on the binder here like
//binder.setGroups(groups);
binder.validate();
return binder.getBindingResult();
}
}
I cant figure out how to get the Validator to use the groups, the seems to be no method for it.
I would like to call it like this.
objectValidator.validate(myObject, Class1.class, Class2.class)
I had a similar issue, and ended up extending the SpringValidatorAdapter class so that I could perform the validation and specify the groups.
ExtendedSpringValidatorAdapter adapter = new ExtendedSpringValidatorAdapter(validator);
adapter.validate(objectToValidate, bindingResult, Class1.class, Class2.class);
Here is the definition of the extended validator:
package com.example.validator;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.groups.Default;
import java.util.Set;
public class ExtendedSpringValidatorAdapter extends SpringValidatorAdapter {
// ========================================================================
// ========================================================================
// ========================================================================
public ExtendedSpringValidatorAdapter(Validator targetValidator) {
super(targetValidator);
}
// ========================================================================
// ========================================================================
// ========================================================================
public void validate(Object target, Errors errors, Class<?>... groups) {
if (groups == null || groups.length == 0 || groups[0] == null) {
groups = new Class<?>[]{Default.class};
}
Set<ConstraintViolation<Object>> result = validate(target, groups);
for (ConstraintViolation<Object> violation : result) {
String field = violation.getPropertyPath().toString();
FieldError fieldError = errors.getFieldError(field);
if (fieldError == null || !fieldError.isBindingFailure()) {
try {
errors.rejectValue(field,
violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(),
getArgumentsForConstraint(errors.getObjectName(), field, violation.getConstraintDescriptor()),
violation.getMessage());
} catch (NotReadablePropertyException ex) {
throw new IllegalStateException("JSR-303 validated property '" + field +
"' does not have a corresponding accessor for Spring data binding - " +
"check your DataBinder's configuration (bean property versus direct field access)", ex);
}
}
}
}
}
I think the easiest way is to use a SmartValidator. You can inject it just like a regular Validator. By using a SmartValidator you have Validation Hints, which you can use to pass the javax.validation.group as parameters. And the final code can be as simple as this:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
#Component
public class FooController {
#Autowired
private SmartValidator validator;
public void validate(MyObject target, Errors errors) {
validator.validate(target, errors, FirstCheck.class, SecondCheck.class);
}
}
Since 3.1 version there are
org.springframework.validation.DataBinder#validate(java.lang.Object...validationHints)
So just do:
binder.validate(Class1.class, Class2.class);
Inside method SmartValidator is used:
if(... validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);