I don't understand what I need to add to make my program become HATEOAS. I have Account.java, Post.java, Controllers and repositories. From some guides they add AccountResource and PostResource where they build links inside these classes. What is the difference between AccountResource and Account? Do I need both? If so, do I create a resource class for each normal class? I tried doing this and it didn't work at all. I have no idea what I'm doing anymore :( . I need some help understanding how to migrate from a normal REST to HATEOAS. What classes do I need to add?
public class Account {
//Account ID
#Id private String userId;
//General info
protected String firstName;
protected String lastName;
protected String username;
protected String email;
protected String password;
protected String birthDate;
protected String activities;
protected String uri;
private Set<Post> posts = new HashSet<>();
List<Account> friends = new ArrayList<Account>();
//Getter, constructor...
#RestController
public class AccountController {
#Autowired
private AccountRepository accountRepository;
//Create account
#RequestMapping(value="/accounts", method = RequestMethod.POST)
public ResponseEntity<?> accountInsert(#RequestBody Account account) {
account = new Account(account.getUri(), account.getUsername(), account.getFirstName(), account.getLastName(), account.getEmail(), account.getPassword(), account.getBirthDate(), account.getActivities(), account.getFriends());
accountRepository.save(account);
HttpHeaders httpHeaders = new HttpHeaders();
Link forOneAccount = new AccountResource(account).getLink("self");
httpHeaders.setLocation(URI.create(forOneAccount.getHref()));
return new ResponseEntity<>(null, httpHeaders, HttpStatus.CREATED);
}
public class AccountResource extends ResourceSupport {
private Account account;
public AccountResource(Account account) {
String username = account.getUsername();
this.account = account;
this.add(new Link(account.getUri(), "account-uri"));
this.add(linkTo(AccountController.class, username).withRel("accounts"));
this.add(linkTo(methodOn(AccountController.class, username).getUniqueAccount(account.getUserId())).withSelfRel());
}
public AccountResource() {
}
public Account getAccount() {
return account;
}
}
Account, Post etc are your domain entities, while the *Resource are their representation exposed by your API to the external world.
Domain entities might be, for example, JPA Entities, containing all the metadata required for their persistency and relations with other entities.
Even if they are not JPA entities, they are the internal representation used by your application business logic.
Resources contains the information for JSON Serialisation/Deserialisation, and no direct references to other resources.
Have a look at this sample project https://github.com/opencredo/spring-hateoas-sample and to the related blog post: Implementing HAL hypermedia REST API using Spring HATEOAS
It implements a simple, but not-so-trivial library API, with a Book - Author - Publisher domain.
Related
I'm learning Spring with JPA. I created a local DB with MySQL storing "Users", a web service with Spring and a front with angular.
I fed some data to my DB, and managed to display it with Angular. But the Post request coming from the Angular form does not seems to work. The form works well and provide an Object.
User model:
#Entity
#Table(name = "utilisateur")
public class Utilisateur {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String firstname;
private String lastname;
private int age;
DAOuser :
#Repository
public interface DAOutilisateur extends JpaRepository<Utilisateur, Integer> {
}
UserController :
#RestController
#CrossOrigin(origins = "http://localhost:4200")
public class UtilisateurController {
DAOutilisateur utilisateurDAO;
#Autowired
public UtilisateurController(final DAOutilisateur utilisateurDAO) {
this.utilisateurDAO = utilisateurDAO;
}
#GetMapping({"/listeUtilisateur"})
public List<Utilisateur> listUtilisateur(){
System.out.println("Liste des utilisateurs");
return utilisateurDAO.findAll();
}
#PostMapping("/listeUtilisateur")
void addUser(#RequestBody Utilisateur user) {
System.out.println("ECHO");
utilisateurDAO.save(user);
}
}
TypeScript Fonction used in Angular to access the Post URL, User is an Object created via an Form:
public saveUserSpring(user: UserSpring) {
return this.http.post<UserSpring>(this.userUrl, user);
}
Thank you for your Help !
Bertrand
Solved
As Rohit Kavathekar mentioned above, I didn't subscribed to the .post method in my angular project, which return an Observable and required to be subscribed.
Thank you.
I want to ignore some fields during deserialization of json data in spring. I cannot use #JsonIgnore as the same model will be used in different methods and different fields need to be ignored.
I have tried to explain the situation with the below example.
class User
{
private String name;
private Integer id;
//getters and setters
}
This is the User class that will be used as model.
#RequestMapping(value = '/path1', method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<CustomResponse> loadUser1(#RequestBody User user){
System.out.println(user.name);
//user.id is not required here
}
This is the first method that will use user.name and ignore user.id.
#RequestMapping(value = '/path2', method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<CustomResponse> loadUser2(#RequestBody User user){
System.out.println(user.id);
//user.name is not required here
}
This is the second method that will use user.id and ignore user.name.
You can use #JsonFilter to achieve dynamic filtering.
First, create a filter using com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter and pass it to a filter provider com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider.
Then, declare this filter on your bean using #JsonFilter.
In your case, this will do it:
#JsonFilter("myFilter")
public class User {
private String name;
private int id;
// Getters and setters
}
This will apply the filter on your POJO:
public MappingJacksonValue getFiltered() {
SimpleFilterProvider filterProvider = new SimpleFilterProvider();
filterProvider.addFilter("myFilter", SimpleBeanPropertyFilter.filterOutAllExcept("id"));
User user = new User();
user.setId(1);
user.setName("Me");
MappingJacksonValue jacksonValue = new MappingJacksonValue(user);
jacksonValue.setFilters(filterProvider);
return jacksonValue;
}
Edit:
SimpleBeanPropertyFilter has factory methods to tender to almost all practical filtering scenarios. Use them appropriately.
I have a user entity that has many attributes (some fields not shown here):
#Entity
public class User {
#OneToOne(cascade = ALL, orphanRemoval = true)
private File avatar; // File is a custom class I have created
#NotEmpty
#NaturalId
private String name;
#Size(min = 6)
private String password;
#Enumerated(EnumType.STRING)
private Role role;
}
In my thymeleaf template I have a form that submits username, password and avatar (MultipartFile). Now in my controller instead of these parameters...
#PostMapping("/register")
public String register(#RequestParam String username,
#RequestParam String password,
#RequestParam MultipartFile avatar) { ...
...I want to use #ModelAttribute #Valid User user. My problem is that:
password first should be encrypted then passed to the user entity,
bytes[] from MultipartFile should be extracted then stored in user entity (as a custom File object),
some other fields such as Role should be set manually in the service class.
How can I take advantage of #ModelAttribute?
Instead of trying to shoehorn everything into your User class, write a UserDto or UserForm which you can convert from/to a User. The UserForm would be specialized for the web and converted to a User later on.
The conversions you are talking about should be done in your controller (as that is ideally only a conversion layer before actually talking to your business services).
public class UserForm {
private MultipartFile avatar;
#NotEmpty
private String username;
#Size(min = 6)
#NotEmpty
private String password;
public UserForm() {}
public UserForm(String username) {
this.username = username;
}
static UserForm of(User user) {
return new UserForm(user.getUsername());
}
// getters/setters omitted for brevity
}
Then in your controller do what you intended to do (something like this):
#PostMapping("/register")
public String register(#ModelAttribute("user") UserForm userForm, BindingResult bindingResult) {
if (!bindingResult.hasErrors()) {
User user = new User();
user.setName(userForm.getUsername());
user.setPassword(encrypt(userForm.getPassword());
user.setAvataor(createFile(userForm.getAvatar());
userService.register(user);
return "success";
} else {
return "register";
}
}
This way you have a specialized object to fix your web based use cases, whilst keeping your actual User object clean.
Maybe you can just use a setter to make all these actions. When Spring is mapping data to fields, and you have setters in the entity, it will use them to pass data. You can preprocess data in this way and set final values to fields.
#PostMapping("/register")
public String register(#ModelAttribute User user, Model model) { // remember if You have another name for parameter and backing bean You should type this one #ModelAttribute(name="userFromTemplate") User user
encryptPassword(user.getPassword); //remember that is sample code, You can do it however You want to
extractBytes(user.getAvatar); //same here
user.setRole(manuallySetRole);
model.addAttribute("user", user);
return "success"; // here u can redirect to ur another html which will contain info about ur user
} else
return "redirect:sorry";
}
encryptPassword(String password) { ... }
same for the rest methods
Here i give You sample code how to use #ModelAttribute in your example. If You have questions feel free to comment.
I am trying to create a springboot application using MongoDB and a Rest controller and connect objects together using DBRef instead of classic Jpa annotations like OneToMany etc. The purpose is to print all the bookmarks for a specific account. The list of bookmarks is found by the username but it seems that it doesn't work.
These are my classes:
#Document
public class Account {
#DBRef
private Set<Bookmark> bookmarkSet = new HashSet<>();
#Id
private String id;
#JsonIgnore
private String username;
private String password;
public Account(String username, String password) {
this.username = username;
this.password = password;
}
public void setBookmarkSet(Set<Bookmark> bookmarkSet) {
this.bookmarkSet = bookmarkSet;
}
public String getId() {
return id;
}
}
#Document
public class Bookmark {
#DBRef
#JsonIgnore
private Account account;
#Id
private String id;
private String uri;
private String description;
public Bookmark(Account account, String uri, String description) {
this.account = account;
this.uri = uri;
this.description = description;
}
public Account getAccount() {
return account;
}
public String getId() {
return id;
}
public String getUri() {
return uri;
}
public String getDescription() {
return description;
}
}
repositories:
public interface AccountRepository extends MongoRepository<Account, Long> {
Optional<Account> findOneByUsername(String username);
}
public interface BookmarkRepository extends MongoRepository<Bookmark, Long> {
Collection<Bookmark> findByAccountUsername(String username);
}
And RestController:
#RestController
#RequestMapping("/{userId}/bookmarks")
public class BookmarkRestController {
private final AccountRepository accountRepository;
private final BookmarkRepository bookmarkRepository;
#Autowired
public BookmarkRestController(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) {
this.accountRepository = accountRepository;
this.bookmarkRepository = bookmarkRepository;
}
#RequestMapping(value = "/{bookmarkId}", method = RequestMethod.GET)
Bookmark readBookmark(#PathVariable String userId, #PathVariable Long bookmarkId) {
this.validateUser(userId);
return bookmarkRepository.findOne(bookmarkId);
}
#RequestMapping(method = RequestMethod.GET)
Collection<Bookmark> readBookmarks(#PathVariable String userId) {
this.validateUser(userId);
return this.bookmarkRepository.findByAccountUsername(userId);
}
private void validateUser(String userId) {
this.accountRepository.findOneByUsername(userId).orElseThrow(() -> new UserNotFoundException(userId));
}
}
After I run the application I get this error:
Invalid path reference account.username! Associations can only be pointed to directly or via their id property!
I'm not sure you have the right schema design. I assume you've modeled you objects based on a relational database type model, where the data is normalised and data is split across multiple tables, with relationships captured using Ids. With MongoDB you can structure and store your data with the heirarchy simply contained in within the one document.
So in your example the Bookmark would not be a Document itself, but would be a sub document of the Account. Remove the #Document annotation from the Bookmark object, and the #DBRef annotations, and simply store the Bookmarks within the Account document.
This would give you a schema more like this:
{
"_id": 1,
"bookmarkSet": [
{
"uri": "http://www.foo.com",
"description": "foo"
},
{
"uri": "http://www.bar.com",
"description": "bar"
}
],
"username": "John",
"password": "password"
}
*Note: if you make the bookmarks sub documents you can remove the _id member from the Bookmark object
The best design will depend on how many bookmarks you expect each account to have. If its only a few bookmarks then what I suggested would work well. If you have thousands then you might want to structure it differently. There are lots of articles about schema design in NoSQL database. This one covers the options for embedding subdocuments quite well:
http://blog.mongodb.org/post/87200945828/6-rules-of-thumb-for-mongodb-schema-design-part-1
I have the following data model, and I want to get a specific object in the sub list objects, I know it's possible to get the entire list and go through each object and compare with what the search id, but I wonder if it is possible use MongoRepository to do this.
#Document
public class Host {
#Id
private String id;
#NotNull
private String name;
#DBRef
private List<Vouchers> listVoucher;
public Host() {
}
//Getters and Setters
}
And..
#Document
public class Vouchers {
#Id
private String id;
#NotNull
private int codeId;
public Vouchers() {
}
//Getters and Setters
}
The Repository Class:
public interface HostRepository extends MongoRepository<Host, String> {
List<Host> findAll();
Host findById(String id);
Host findByName(String name);
//How to build the correct query ??????????
List<Vouchers> findVouchersAll();
Vouchers findByVouchersById(String hostId, String voucherId);
}
The Controller Class:
#RestController
#RequestMapping(value = "api/v1/host")
public class VoucherController {
#Inject
HostRepository hostRepository;
#RequestMapping(value = "/{hostId}/voucher",method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public List<Vouchers> list() {
return hostRepository.findVouchersAll();
}
#RequestMapping(value = "/{hostId}/voucher/{voucherId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public Vouchers getOneVoucher(#PathVariable String hostId, #PathVariable String voucherId) {
Vouchers voucher = hostRepository.findByVouchersById(hostId, voucherId);
if (voucher != null) {
return voucher;
} else {
throw new VoucherNotFoundException(String.format("There is no voucher with id=%s", voucherId));
}
}
}
Thanks in Advance!
I think there is a way to do this although I have not tried this myself but maybe I can shed some light in how I would do it.
Firstly, I would rather use the more flexible way of querying mongodb by using MongoTemplate. MongoTemplate is already included in the Spring Boot Mongodb data library and it looks like you are already using the library so it is not an additional library that you will have to use. In Spring there is a way to #Autowired your MongoTemplate up so it is quick and easy to get the object for completing this task.
With mongoTemplate, you would do something like this:
Query query = new Query();
query.addCriteria(Criteria.where("listVouchers.id").is("1234"));
List<Host> host = mongoTemplate.find(query, Host.class);
Please see docs here: https://docs.mongodb.org/manual/tutorial/query-documents/