#65 - Added example project for Spring Data web support.

This commit is contained in:
Oliver Gierke
2015-03-09 11:04:00 +01:00
parent 2c97493f9f
commit df7fcb0edb
17 changed files with 815 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example;
import java.util.stream.IntStream;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import example.users.Password;
import example.users.UserManagement;
import example.users.Username;
/**
* Central Spring Boot application class to bootstrap the application. Excludes Spring Security auto-configuration as we
* don't need it for the example but only want to use a {@link PasswordEncoder} (see {@link #passwordEncoder()}).
* <p>
* Spring Data web support is transparently activated by Boot for you. In case you want to manually activate it, use
* {@link EnableSpringDataWebSupport}. The core aspects of the enabled functionality shown in this example are:
* <ol>
* <li>Automatic population of a {@link Pageable} instances from request parameters (see
* {@link example.users.web.UserController#users(Pageable)})</li>
* <li>The ability to use proxy-backed interfaces to bind request payloads (see
* {@link example.users.web.UserController.UserForm})</li>
* </ol>
*
* @author Oliver Gierke
*/
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class Application {
public static void main(String... args) {
SpringApplication.run(Application.class, args);
}
@Autowired UserManagement userManagement;
/**
* Creates a few sample users.
*/
@PostConstruct
public void init() {
IntStream.range(0, 41).forEach(index -> {
userManagement.register(new Username("user" + index), Password.raw("foobar"));
});
}
/**
* A Spring Security {@link PasswordEncoder} to encrypt passwords for newly created users, used in
* {@link UserManagement}.
*
* @return
*/
public @Bean PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import javax.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Delegate;
/**
* A value object to represent {@link Password}s in encrypted and unencrypted state. Note how the methods to create a
* {@link Password} in encrypted state are restricted to package scope so that only the user subsystem is actually able
* to encrypted passwords.
*
* @author Oliver Gierke
*/
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter(AccessLevel.PACKAGE)
@Embeddable
public class Password implements CharSequence {
private @Delegate final String password;
private @Getter transient boolean encrypted;
Password() {
this.password = null;
this.encrypted = true;
}
/**
* Creates a new raw {@link Password} for the given source {@link String}.
*
* @param password must not be {@literal null} or empty.
* @return
*/
public static Password raw(String password) {
return new Password(password, false);
}
/**
* Creates a new encrypted {@link Password} for the given {@link String}. Note how this method is package protected so
* that encrypted passwords can only created by components in this package and not accidentally by clients using the
* type from other packages.
*
* @param password must not be {@literal null} or empty.
* @return
*/
static Password encrypted(String password) {
return new Password(password, true);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return encrypted ? password : "********";
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* A {@link User} domain object. The primary entity of this example. Basically a combination of a {@link Username} and
* {@link Password}.
*
* @author Oliver Gierke
*/
@Entity
@Getter
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@EqualsAndHashCode(of = "id")
public class User {
private @GeneratedValue @Id Long id;
private final Username username;
private final Password password;
User() {
this.username = null;
this.password = null;
}
/**
* Makes sure only {@link User}s with encrypted {@link Password} can be persisted.
*/
@PrePersist
@PreUpdate
void assertEncrypted() {
if (!password.isEncrypted()) {
throw new IllegalStateException("Tried to persist/load a user with a non-encrypted password!");
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import java.util.Optional;
import javax.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
/**
* Domain service to register {@link User}s in the system.
*
* @author Oliver Gierke
*/
@Transactional
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserManagement {
private final UserRepository repository;
private final PasswordEncoder encoder;
/**
* Registers a {@link User} with the given {@link Username} and {@link Password}.
*
* @param username must not be {@literal null}.
* @param password must not be {@literal null}.
* @return
*/
public User register(Username username, Password password) {
Assert.notNull(username, "Username must not be null!");
Assert.notNull(password, "Password must not be null!");
repository.findByUsername(username).ifPresent(user -> {
throw new IllegalArgumentException("User with that name already exists!");
});
Password encryptedPassword = Password.encrypted(encoder.encode(password));
return repository.save(new User(username, encryptedPassword));
}
/**
* Returns a {@link Page} of {@link User} for the given {@link Pageable}.
*
* @param pageable must not be {@literal null}.
* @return
*/
public Page<User> findAll(Pageable pageable) {
Assert.notNull(pageable, "Pageable must not be null!");
return repository.findAll(pageable);
}
/**
* Returns the {@link User} with the given {@link Username}.
*
* @param username must not be {@literal null}.
* @return
*/
public Optional<User> findByUsername(Username username) {
Assert.notNull(username, "Username must not be null!");
return repository.findByUsername(username);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import java.util.Optional;
import org.springframework.data.repository.PagingAndSortingRepository;
/**
* A Spring Data repository to persist {@link User}s.
*
* @author Oliver Gierke
*/
interface UserRepository extends PagingAndSortingRepository<User, Long> {
/**
* Returns the user with the given {@link Username}.
*
* @param username can be {@literal null}.
* @return
*/
Optional<User> findByUsername(Username username);
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import javax.persistence.Embeddable;
import lombok.EqualsAndHashCode;
import org.springframework.util.StringUtils;
/**
* value object to represent user names.
*
* @author Oliver Gierke
*/
@EqualsAndHashCode
@Embeddable
public class Username {
private final String username;
Username() {
this.username = null;
}
/**
* Creates a new {@link Username}.
*
* @param username must not be {@literal null} or empty.
*/
public Username(String username) {
if (!StringUtils.hasText(username)) {
throw new IllegalArgumentException("Invalid username!");
}
this.username = username;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return username;
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users.web;
import static org.springframework.validation.ValidationUtils.*;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.view.RedirectView;
import example.users.Password;
import example.users.User;
import example.users.UserManagement;
import example.users.Username;
/**
* A sample controller implementation to showcase Spring Data web support:
* <ol>
* <li>Automatic population of a {@link Pageable} instance as controller method argument. This is achieved by the
* automatic activation of {@link EnableSpringDataWebSupport} and in turn its registration of a
* {@link PageableHandlerMethodArgumentResolver}.</li>
* <li>Usage of proxy-backed interfaces to bind request parameters.</li>
* </ol>
*
* @author Oliver Gierke
*/
@Controller
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@RequestMapping("/users")
class UserController {
private final UserManagement userManagement;
/**
* Equis the model with a {@link Page} of {@link User}s. Spring Data automatically populates the {@link Pageable} from
* request data according to the setup of {@link PageableHandlerMethodArgumentResolver}. Note how the defaults can be
* tweaked by using {@link PageableDefault}.
*
* @param pageable will never be {@literal null}.
* @return
*/
@ModelAttribute("users")
public Page<User> users(@PageableDefault(size = 5) Pageable pageable) {
return userManagement.findAll(pageable);
}
/**
* Registers a new {@link User} for the data provided by the given {@link UserForm}. Note, how an interface is used to
* bind request parameters.
*
* @param form the request data bound to the {@link UserForm} instance.
* @param binding the result of the binding operation.
* @param model the Spring MVC {@link Model}.
* @return
*/
@RequestMapping(method = RequestMethod.POST)
public Object register(UserForm userForm, BindingResult binding, Model model) {
userForm.validate(binding, userManagement);
if (binding.hasErrors()) {
return "users";
}
userManagement.register(new Username(userForm.getUsername()), Password.raw(userForm.getPassword()));
RedirectView redirectView = new RedirectView("redirect:/users");
redirectView.setPropagateQueryParams(true);
return redirectView;
}
/**
* Populates the {@link Model} with the {@link UserForm} automatically created by Spring Data web components. It will
* create a {@link Map}-backed proxy for the interface.
*
* @param model will never be {@literal null}.
* @param userForm will never be {@literal null}.
* @return
*/
@RequestMapping(method = RequestMethod.GET)
public String listUsers(Model model, UserForm userForm) {
model.addAttribute("userForm", userForm);
return "users";
}
/**
* An interface to represent the form to be used
*
* @author Oliver Gierke
*/
interface UserForm {
String getUsername();
String getPassword();
String getRepeatedPassword();
/**
* Validates the {@link UserForm}.
*
* @param errors
* @param userManagement
*/
default void validate(BindingResult errors, UserManagement userManagement) {
rejectIfEmptyOrWhitespace(errors, "username", "user.username.empty");
rejectIfEmptyOrWhitespace(errors, "password", "user.password.empty");
rejectIfEmptyOrWhitespace(errors, "repeatedPassword", "user.repeatedPassword.empty");
if (!getPassword().equals(getRepeatedPassword())) {
errors.rejectValue("repeatedPassword", "user.password.no-match");
}
try {
userManagement.findByUsername(new Username(getUsername())).ifPresent(
user -> errors.rejectValue("username", "user.username.exists"));
} catch (IllegalArgumentException o_O) {
errors.rejectValue("username", "user.username.invalidFormat");
}
}
}
}