Here is my code for creating a custom Annotation for validating Name
ValidName.java
package custom.Annotation;
import java.lang.annotation.*;
import net.sf.oval.configuration.annotation.Constraint;
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.LOCAL_VARIABLE)
#Constraint(checkWith=NameValidator.class)
public #interface ValidName {
String message() default NameValidator.message;
}
here is my code for Constraint Class
package custom.Annotation;
import net.sf.oval.Validator;
import net.sf.oval.configuration.annotation.AbstractAnnotationCheck;
import net.sf.oval.context.OValContext;
import net.sf.oval.exception.OValException;
import play.Logger;
import java.util.regex.Pattern;
public class NameValidator extends AbstractAnnotationCheck<ValidName>
{
public final static String message="custom.message";
private static final String letter = "[a-zA-Z]";
public static final Pattern VALID_PATTERN = Pattern.compile(letter);
public static boolean isValidText(String userName) {
return VALID_PATTERN.matcher(userName).matches();
}
#Override
public void configure(ValidName annotation) {
setMessage(annotation.message());
}
#Override
public boolean isSatisfied(Object validatedObject, Object valueToValidate, OValContext context,
Validator validator) throws OValException {
try
{
if (valueToValidate == null) {
return false;
}
}catch (Exception e){
e.getMessage();
}
return` isValidText(valueToValidate.toString()`);
}
}
When I applied #ValidName to any local variable nothing happened
and I also am unable to debug the program. Any suggestions?
You need to use the oval validation by calling validate method of oval validation library.
#Autowired
#Qualifier("ovalValidator")
private Validator ovalValidator;
List<ConstraintViolation> violations = null;
violations = ovalValidator.validate(objectToValidate);
Related
I've been reading a lot about spring request validation. I read a lot articles on how to appropriately implement that, but I have some problem. This is my code:
RestController:
#Autowired
EmployeeManager employeeManager;
#Autowired
EmployeeValidator employeeValidator;
#InitBinder("employee")
public void setupBinder(WebDataBinder binder) {
binder.addValidators(employeeValidator);
}
// -------------- CREATE EMPLOYEES --------------
#PostMapping(value = "add")
public ResponseEntity<EmployeeDTO> addEmployee(#Valid #RequestBody EmployeeDTO employee) {
boolean isCreated = employeeManager.addEmployee(employee);
if(isCreated) {
return new ResponseEntity<>(employee, HttpStatus.CREATED);
}
return new ResponseEntity(new CustomError("Unable to create, employee with email " +
employee.getEmail() + " already exist."), HttpStatus.CONFLICT);
}
Validator:
package com.employee.api.EmployeeAPI.validator;
import com.employee.api.EmployeeAPI.model.dto.EmployeeDTO;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
#Component
public class EmployeeValidator implements Validator {
private Pattern pattern;
private Matcher matcher;
private static final String STRING_PATTERN = "[a-zA-Z]+";
private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*#"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
#Override
public boolean supports(Class<?> clazz) {
return EmployeeDTO.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
EmployeeDTO employee = (EmployeeDTO) target;
if (validateInputString(employee.getFirstName(), STRING_PATTERN)) {
errors.rejectValue("firstName", "firstName.invalid");
}
if (validateInputString(employee.getLastName(), STRING_PATTERN)) {
errors.rejectValue("lastName", "lastName.invalid");
}
if (validateInputString(employee.getJob(), STRING_PATTERN)) {
errors.rejectValue("job", "job.invalid");
}
if (validateInputString(employee.getEmail(), EMAIL_PATTERN)) {
errors.rejectValue("email", "email.invalid");
}
}
private boolean validateInputString(String input, String regexPattern) {
pattern = Pattern.compile(regexPattern);
matcher = pattern.matcher(input);
return (!matcher.matches() || input == null || input.trim().length() == 0);
}
}
and in config I added bean:
#Bean
public EmployeeValidator beforeAddOrUpdateEmployeeValidator() {
return new EmployeeValidator();
}
I am not really sure of how it should be invoked right now when adding employees, because it surely does not work for now. Could you help me with the right implementation or point in the right direction?
I'm not familiar with org.springframework.validation.Validator, but will suggest you how to do the same validation as you need with javax.validation.ConstraintValidator (JSR-303). Your controller class is fine and no changes needed there.
you need to create a custom annotation #ValidEmployee and annotate your dto with it:
#ValidEmployee
public class EmployeeDto {
...
}
ValidEmployee annotation:
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = EmployeeValidator.class)
#Documented
public #interface ValidEmployee {
String message() default "{ValidEmployee.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and implement your validation logic in isValid method:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmployeeValidator implements ConstraintValidator<ValidEmployee, EmployeeDto> {
#Override
public void initialize(ValidEmployee constraintAnnotation) {
}
#Override
public boolean isValid(EmployeeDto employee, ConstraintValidatorContext context) {
// do your validation logic
}
}
See the example below, I'm trying to get a Map of my TypedService beans but I would prefer if the keys were the Type enum values specified in the TypeSafeQualifier instead of the unsafe String "serviceName".
package org.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Service;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Map;
import static org.test.Application.Type.ONE;
import static org.test.Application.Type.TWO;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#SpringBootApplication
public class Application {
#Autowired
Map<String, TypedService> works;
#Autowired
Map<Type, TypedService> fails;
public static void main(String [] args) {
SpringApplication.run(Application.class, args);
}
public enum Type {
ONE,
TWO
}
#Target({TYPE, METHOD, FIELD, CONSTRUCTOR})
#Retention(RUNTIME)
#Qualifier
public #interface TypeSafeQualifier {
Type value();
}
public interface TypedService {
void startSignup();
void activate();
}
#Service
#TypeSafeQualifier(ONE)
public class TypeOneService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
#Service
#TypeSafeQualifier(TWO)
public class TypeTwoService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
}
SpringBoot version: springBootVersion=1.5.3.RELEASE
Spring offers a special approach to handle this type of injection: AutowireCandidateResolver.
In your case the code might be:
package org.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.LinkedHashMap;
import java.util.Map;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#SpringBootApplication
public class Application {
#Autowired
Map<String, TypedService> works;
#Autowired
Map<Type, TypedService> fails;
#PostConstruct
private void init() {
System.out.println(fails);
}
public static void main(String[] args) {
final SpringApplication application = new SpringApplication(Application.class);
application.addInitializers(context -> {
context.addBeanFactoryPostProcessor(beanFactory -> {
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
dlbf.setAutowireCandidateResolver(new MyAutowireCandidateResolver(dlbf));
});
});
application.run(args);
}
#QualifierValue(TypeSafeQualifier.class)
public enum Type {
ONE,
TWO
}
#Target({TYPE, METHOD, FIELD, CONSTRUCTOR})
#Retention(RUNTIME)
#Qualifier
public #interface TypeSafeQualifier {
Type value();
}
public interface TypedService {
void startSignup();
void activate();
}
#Service
#TypeSafeQualifier(Type.ONE)
public class TypeOneService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
#Target({TYPE})
#Retention(RUNTIME)
public #interface QualifierValue {
Class<? extends Annotation> value();
}
#Service
#TypeSafeQualifier(Type.TWO)
public class TypeTwoService implements TypedService {
#Override
public void startSignup() {
}
#Override
public void activate() {
}
}
private static class MyAutowireCandidateResolver extends ContextAnnotationAutowireCandidateResolver {
private final DefaultListableBeanFactory beanFactory;
private MyAutowireCandidateResolver(DefaultListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
#Override
public Object getSuggestedValue(DependencyDescriptor descriptor) {
final Object result = super.getSuggestedValue(descriptor);
if (result != null) {
return result;
}
if (descriptor.getDependencyType() != Map.class) {
return null;
}
final ResolvableType dependencyGenericType = descriptor.getResolvableType().asMap();
final ResolvableType[] typeParams = dependencyGenericType.getGenerics();
final QualifierValue qualifierValue = typeParams[0].getRawClass().getAnnotation(QualifierValue.class);
if (qualifierValue == null) {
return null;
}
final String[] candidateBeanNames = beanFactory.getBeanNamesForType(typeParams[1]);
final LinkedHashMap<Object, Object> injectedMap = new LinkedHashMap<>(candidateBeanNames.length);
for (final String candidateBeanName : candidateBeanNames) {
final Annotation annotation = beanFactory.findAnnotationOnBean(candidateBeanName, qualifierValue.value());
if (annotation == null) {
continue;
}
final Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation, false);
final Object value = annotationAttributes.get("value");
if (value == null || value.getClass() != typeParams[0].getRawClass()) {
continue;
}
injectedMap.put(value, beanFactory.getBean(candidateBeanName));
}
return injectedMap;
}
}
}
First of all, we add TypeQualifierValue annotation to make Spring know about a qualifier with values of the given type.
The second is to customize the SpringApplication in the main method: we use BeanFactoryPostProcessor to set a custom AutowireCandidateResolver.
And the final step: we write MyAutowireCandidateResolver extending ContextAnnotationAutowireCandidateResolver (delegation instead of inheritance is applicable to, it's even a little bit better since one day Spring can migrate to `YetAnotherAutowireCandidateResolver' by default).
The crucial part here is the overridden getSuggestedValue method: here we can customize the injection logic considering the generic types of the dependency (field, method parameter) and by applying some getBean...-like methods from the BeanFactory with some magic of Spring AnnotationUtils class.
In Spring boot 1.3.6-RELEASE I had the below class registered to jersey. Every java.util.Date field would be read and returned as ISO8601 format. However, when updating to 1.4.1-RELEASE it now sometimes works and sometimes doesn't. What's the new proper way to enable this?
package com.mypackage;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.util.ISO8601Utils;
#Provider
public class DateTimeParamConverterProvider implements ParamConverterProvider {
#SuppressWarnings("unchecked")
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (type.equals(Date.class)) {
return (ParamConverter<T>) new DateTimeParamConverter();
} else {
return null;
}
}
static class DateTimeParamConverter implements ParamConverter<Date> {
#Override
public java.util.Date fromString(String value) {
if (value == null) {
return null;
}
try {
return ISO8601Utils.parse(value, new ParsePosition(0));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
#Override
public String toString(Date value) {
return ISO8601Utils.format(value);
}
}
}
I register this provider like this:
#Component
#ApplicationPath("/")
public class JerseyConfiguration extends ResourceConfig {
private static final Logger log = Logger.getLogger(JerseyConfiguration.class.getName());
#Autowired
public JerseyConfiguration(LogRequestFilter lrf) {
register(new ObjectMapperContextResolverNonNull());
register(RestServiceImpl.class);
property(ServletProperties.FILTER_FORWARD_ON_404, true);
register(DateTimeParamConverterProvider.class, 6000);
...
Just define this in your application.properties:
spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat
I use:
Gwt-platform
Gwt Validation
Gwt UiEditor framework
Gwt Material inputs
After constraints validation I would like to display error messages. This is supported by gwt material inputs, like MaterialTextBox using method:  materialTextBox.setError("Please provide your name");
The problem is that it can be only executed from view class:
public class LoginView extends ViewWithUiHandlers<LoginUiHandlers> implements LoginPresenter.MyView {
interface Binder extends UiBinder<Widget, LoginView> {}
/** The driver to link the proxy bean with the view. */
public interface EditorDriver extends SimpleBeanEditorDriver<LoginModel, LoginView> { }
#UiField MaterialTextBox email;
#UiField MaterialTextBox password;
#UiField MaterialButton loginButton;
#UiField MaterialCheckBox keepMeLoggedInCheckbox;
#Inject
LoginView(Binder uiBinder) {
initWidget(uiBinder.createAndBindUi(this));
addClickHandlerToLoginButton();
}
//#UiHandler("loginButton")
private void onLoginButtonClick(ClickEvent e){
getUiHandlers().onLoginButtonClick();
}
private void addClickHandlerToLoginButton() {
loginButton.addClickHandler(new ClickHandler() {
#Override public void onClick(ClickEvent event) {
onLoginButtonClick(event);
}
});
}
#Override
public SimpleBeanEditorDriver<LoginModel, ?> createEditorDriver() {
EditorDriver driver = GWT.create(EditorDriver.class);
driver.initialize(this);
return driver;
}
public void test() {}
}
But I do all my editor/validation action in Presenter, and I do not have there any View connection I could use:
public class LoginPresenter extends Presenter<LoginPresenter.MyView, LoginPresenter.MyProxy> implements LoginUiHandlers {
public interface MyView extends BeanEditView<LoginModel>, HasUiHandlers<LoginUiHandlers> {}
public static final Type<RevealContentHandler<?>> SLOT_Login = new Type<RevealContentHandler<?>>();
#ProxyStandard
#NameToken(NameTokens.login)
public interface MyProxy extends ProxyPlace<LoginPresenter> {}
// Editor
private SimpleBeanEditorDriver<LoginModel, ?> editorDriver;
private static final LoginService service = GWT.create(LoginService.class);
private LoginModel model = new LoginModel("","");
#Override
public void onLoginButtonClick() {
if (editorDriver.isDirty()) {
model = editorDriver.flush();
validateModel();
if (editorDriver.hasErrors()) {
MaterialToast.fireToast("Errors occur");
StringBuilder errorBuilder = new StringBuilder();
for (EditorError error : editorDriver.getErrors()) {
errorBuilder.append(error.getMessage() + "\n");
}
MaterialToast.fireToast(errorBuilder.toString());
} else {
service.login(
model, new MethodCallback<Integer>() {
#Override
public void onSuccess(Method method, Integer response) {
MaterialToast.fireToast("Succefully set info. status code: " + response);
}
#Override
public void onFailure(Method method, Throwable exception) {
MaterialToast.fireToast("Error setting");
}
});
}
} else {
MaterialToast.fireToast("Data has not changed");
}
}
private void validateModel() {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<LoginModel>> violations = validator.validate(model);
if (violations.size() > 0) {
editorDriver.setConstraintViolations(new ArrayList<ConstraintViolation<?>>(violations));
}
}
#Inject
LoginPresenter(EventBus eventBus,MyView view, MyProxy proxy) {
super(eventBus, view, proxy, RevealType.Root);
getView().setUiHandlers(this);
editorDriver = getView().createEditorDriver();
editorDriver.edit(model);
}
}
I think that I should add an interface where I will declare methods to access inputs in view. And do implementation of that. But I don't know how. Please help me.
I've make it working. But really I am not quite sure what did happened that it start to work. So I will just share my working code:
Presenter
package pl.korbeldaniel.cms.client.login;
import java.util.ArrayList;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import org.fusesource.restygwt.client.Method;
import org.fusesource.restygwt.client.MethodCallback;
import gwt.material.design.client.ui.MaterialToast;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.editor.client.SimpleBeanEditorDriver;
import com.google.gwt.event.shared.GwtEvent.Type;
import com.google.inject.Inject;
import com.google.web.bindery.event.shared.EventBus;
import com.gwtplatform.mvp.client.Presenter;
import com.gwtplatform.mvp.client.annotations.ProxyStandard;
import com.gwtplatform.mvp.client.proxy.ProxyPlace;
import com.gwtplatform.mvp.client.annotations.NameToken;
import com.gwtplatform.mvp.client.proxy.RevealContentHandler;
import com.gwtplatform.mvp.client.HasUiHandlers;
import pl.korbeldaniel.cms.client.editor.BeanEditView;
import pl.korbeldaniel.cms.client.model.LoginModel;
import pl.korbeldaniel.cms.client.place.NameTokens;
import pl.korbeldaniel.cms.client.service.LoginService;
public class LoginPresenter extends Presenter<LoginPresenter.MyView, LoginPresenter.MyProxy> implements LoginUiHandlers {
#ProxyStandard
#NameToken(NameTokens.login)
public interface MyProxy extends ProxyPlace<LoginPresenter> {}
public interface MyView extends BeanEditView<LoginModel>, HasUiHandlers<LoginUiHandlers> {}
public static final Type<RevealContentHandler<?>> SLOT_Login = new Type<RevealContentHandler<?>>();
// Editor
private SimpleBeanEditorDriver<LoginModel, ?> editorDriver;
private static final LoginService service = GWT.create(LoginService.class);
private LoginModel model = new LoginModel();
#Override
public void onLoginButtonClick() {
if (editorDriver.isDirty()) {
model = editorDriver.flush();
validateModel();
if (editorDriver.hasErrors()) {
MaterialToast.fireToast("Errors occur");
} else {
service.login(
model, new MethodCallback<Integer>() {
#Override
public void onSuccess(Method method, Integer response) {
MaterialToast.fireToast("Succefully set info. status code: " + response);
}
#Override
public void onFailure(Method method, Throwable exception) {
MaterialToast.fireToast("Error setting");
}
});
}
} else {
MaterialToast.fireToast("Data has not changed");
}
}
private void validateModel() {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<LoginModel>> violations = validator.validate(model);
GWT.log(String.valueOf(violations.size()));
if (violations.size() > 0) {
editorDriver.setConstraintViolations(new ArrayList<ConstraintViolation<?>>(violations));
}
model.validate();
}
#Inject
LoginPresenter(EventBus eventBus,MyView view, MyProxy proxy) {
super(eventBus, view, proxy, RevealType.Root);
getView().setUiHandlers(this);
editorDriver = getView().createEditorDriver();
editorDriver.edit(model);
}
public enum EditorMode {
VIEW, EDIT, CREATE
};
}
Ui Handler
package pl.korbeldaniel.cms.client.login;
import com.gwtplatform.mvp.client.UiHandlers;
interface LoginUiHandlers extends UiHandlers {
void onLoginButtonClick();
}
ValidatorFactory
package pl.korbeldaniel.cms.client.login;
import javax.validation.Validator;
import pl.korbeldaniel.cms.client.model.LoginModel;
import com.google.gwt.core.client.GWT;
import com.google.gwt.validation.client.AbstractGwtValidatorFactory;
import com.google.gwt.validation.client.GwtValidation;
import com.google.gwt.validation.client.impl.AbstractGwtValidator;
public final class SampleValidatorFactory extends AbstractGwtValidatorFactory {
/**
* Validator marker for the Validation Sample project. Only the classes and
* groups listed in the {#link GwtValidation} annotation can be validated.
*/
#GwtValidation(LoginModel.class)
public interface GwtValidator extends Validator {
}
#Override
public AbstractGwtValidator createValidator() {
return GWT.create(GwtValidator.class);
}
}
View
package pl.korbeldaniel.cms.client.login;
import gwt.material.design.client.ui.MaterialButton;
import gwt.material.design.client.ui.MaterialCheckBox;
import gwt.material.design.client.ui.MaterialTextBox;
import javax.inject.Inject;
import pl.korbeldaniel.cms.client.model.LoginModel;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.SimpleBeanEditorDriver;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Widget;
import com.gwtplatform.mvp.client.ViewWithUiHandlers;
public class LoginView extends ViewWithUiHandlers<LoginUiHandlers> implements LoginPresenter.MyView {
interface Binder extends UiBinder<Widget, LoginView> {}
/** The driver to link the proxy bean with the view. */
public interface EditorDriver extends SimpleBeanEditorDriver<LoginModel, LoginView> { }
#UiField MaterialTextBox email;
#UiField MaterialTextBox password;
#UiField MaterialButton loginButton;
#UiField MaterialCheckBox keepMeLoggedInCheckbox;
#Inject
LoginView(Binder uiBinder) {
initWidget(uiBinder.createAndBindUi(this));
addClickHandlerToLoginButton();
}
//#UiHandler("loginButton")
private void onLoginButtonClick(ClickEvent e){
getUiHandlers().onLoginButtonClick();
}
private void addClickHandlerToLoginButton() {
loginButton.addClickHandler(new ClickHandler() {
#Override public void onClick(ClickEvent event) {
onLoginButtonClick(event);
}
});
}
#Override
public SimpleBeanEditorDriver<LoginModel, ?> createEditorDriver() {
EditorDriver driver = GWT.create(EditorDriver.class);
driver.initialize(this);
return driver;
}
}
View Binder
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:m="urn:import:gwt.material.design.client.ui"
xmlns:e="urn:import:pl.korbeldaniel.cms.client.login">
<m:MaterialRow ui:field="loginWidget">
<m:MaterialColumn grid="s12 m4 l4" offset="l4 m4" >
<m:MaterialTitle title="Login" description="Please provide your account credentials."/>
<m:MaterialPanel padding="5" shadow="1" addStyleNames="{style.panel}">
<m:MaterialPanel addStyleNames="{style.fieldPanel}">
<!-- <m:MaterialImage url="http://b.vimeocdn.com/ps/339/488/3394886_300.jpg" type="CIRCLE" addStyleNames="{style.imgProfile} z-depth-1"/> -->
<m:MaterialTextBox ui:field="email" type="EMAIL" placeholder="Email"/>
<m:MaterialTextBox ui:field="password" type="PASSWORD" placeholder="Password"/>
<m:MaterialRow addStyleNames="{style.rowAction}">
<m:MaterialColumn grid="s12 m12 l6">
<m:MaterialCheckBox ui:field="keepMeLoggedInCheckbox" text="Keep me logged in"/>
</m:MaterialColumn>
</m:MaterialRow>
<m:MaterialButton ui:field="loginButton" waves="LIGHT" text="Log In" width="100%"/>
</m:MaterialPanel>
</m:MaterialPanel>
</m:MaterialColumn>
</m:MaterialRow>
</ui:UiBinder>
I have configured a RepositoryRestResource on a PageAndSortingRepository that accesses an Entity that includes a composite Id:
#Entity
#IdClass(CustomerId.class)
public class Customer {
#Id BigInteger id;
#Id int startVersion;
...
}
public class CustomerId {
BigInteger id;
int startVersion;
...
}
#RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}
When i access the server at "http://<server>/api/customers/1_1" for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "http://<server>/api/customer/1"
i.e.:
{
"id" : 1,
"startVersion" : 1,
...
"firstname" : "BOB",
"_links" : {
"self" : {
"href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
}
}
}
I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.
I've had a look at the ResourceSupport and the ResourceProcessor class but am not sure how much i need to change in order fix this issue.
Can someone who knows spring lend me a hand?
Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box.
The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id and EmbeddedId as identifier.
Excerpt JpaPersistentPropertyImpl:
static {
// [...]
annotations = new HashSet<Class<? extends Annotation>>();
annotations.add(Id.class);
annotations.add(EmbeddedId.class);
ID_ANNOTATIONS = annotations;
}
Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.
Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.
Here's the hack. This should give you an idea how to tackle your problem.
In your configuration override repositoryExporterHandlerAdapter and return a CustomPersistentEntityResourceAssemblerArgumentResolver.
Additionally, override backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter:
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
#Configuration
#Import(RepositoryRestMvcConfiguration.class)
#EnableSpringDataWebSupport
public class RestConfig extends RepositoryRestMvcConfiguration {
#Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
#Autowired
ListableBeanFactory beanFactory;
#Override
#Bean
public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3);
converters.add(new CustomBackendIdConverter());
converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
#Bean
public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
configureHttpMessageConverters(messageConverters);
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
resourceProcessors);
handlerAdapter.setMessageConverters(messageConverters);
return handlerAdapter;
}
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
{
CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));
return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver, backendIdHandlerMethodArgumentResolver());
}
}
Create CustomBackendIdConverter. This class is responsible for rendering your custom entity ids:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import java.io.Serializable;
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return id;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if(entityType.equals(Customer.class)) {
Customer c = (Customer) id;
return c.getId() + "_" +c.getStartVersion();
}
return id.toString();
}
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
}
CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler:
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {
super(repositories, entityLinks,projectionDefinitions,projectionFactory);
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
}
public boolean supportsParameter(MethodParameter parameter) {
return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter);
return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector);
}
}
CustomPersistentEntityResourceAssembler needs to override getSelfLinkFor. As you can see entity.getIdProperty() return either id or startVersion property of your Customer class which in turn gets used to retrieve the real value with the help of a BeanWrapper. Here we are short circuit the whole framework with the use of instanceof operator. Hence your Customer class should implement Serializable for further processing.
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler {
private final Repositories repositories;
private final EntityLinks entityLinks;
public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
super(repositories, entityLinks, projector);
this.repositories = repositories;
this.entityLinks = entityLinks;
}
public Link getSelfLinkFor(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
}
Object id;
//this is a hack for demonstration purpose. don't do this at home!
if(instance instanceof Customer) {
id = instance;
} else {
BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null);
id = wrapper.getProperty(entity.getIdProperty());
}
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
}
}
That's it! You should see this URIs:
{
"_embedded" : {
"customers" : [ {
"name" : "test",
"_links" : {
"self" : {
"href" : "http://localhost:8080/demo/customers/1_1"
}
}
} ]
}
}
Imho, if you are working on a green field project I would suggest to ditch IdClass entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.
Although not desirable, I have worked around this issue by using an #EmbeddedId instead of a IdClass annotation on my JPA entity.
Like so:
#Entity
public class Customer {
#EmbeddedId
private CustomerId id;
...
}
public class CustomerId {
#Column(...)
BigInteger key;
#Column(...)
int startVersion;
...
}
I now see the correctly generated links 1_1 on my returned entities.
If anyone can still direct me to a solution that does not require I change the representation of my model, It would be highly appreciated. Luckily I had not progressed far in my application development for this to be of serious concern in changing, but I imagine that for others, there would be significant overhead in performing a change like this: (e.g. changing all queries that reference this model in JPQL queries).
I had a similar problem where the composite key scenarios for data rest was not working. #ksokol detailed explanation provided the necessary inputs to solve the issue. changed my pom primarily for data-rest-webmvc and data-jpa as
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
which solved all the issues related to composite key and I need not do the customization. Thanks ksokol for the detailed explanation.
First, create a SpringUtil to get bean from spring.
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
#Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
Then, implement BackendIdConverter.
import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
#Component
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id == null) {
return null;
}
//first decode url string
if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
try {
id = URLDecoder.decode(id, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//deserialize json string to ID object
Object idObject = null;
for (Method method : entityType.getDeclaredMethods()) {
if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
idObject = JSON.parseObject(id, method.getGenericReturnType());
break;
}
}
//get dao class from spring
Object daoClass = null;
try {
daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//get the entity with given primary key
JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
Object entity = simpleJpaRepository.findOne((Serializable) idObject);
return (Serializable) entity;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id == null) {
return null;
}
String jsonString = JSON.toJSONString(id);
String encodedString = "";
try {
encodedString = URLEncoder.encode(jsonString, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodedString;
}
}
After that. you can do what you want.
There is a sample below.
If the entity has single property pk, you can use
localhost:8080/demo/1 as normal. According to my code, suppose the pk
has annotation "#Id".
If the entity has composed pk, suppose the pk is demoId type, and has
annotation "#EmbeddedId", you can use localhost:8080/demo/{demoId
json} to get/put/delete. And your self link will be the same.
The answers provides above are helpful, but if you need a more generic approach that 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();
}
}