Kihagyás

6. gyakorlat

CRUD REST API végpontok

Listázás

Feladat

Készítsünk egy egyszerű REST végpontot, mely visszaadja az összes létező Contact-ot az adatbázisból!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package hu.suaf.contacts.rest;

import hu.suaf.contacts.model.Contact;
import hu.suaf.contacts.service.ContactService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/api/contact", produces = "application/json")
public class ContactRestController {
    private ContactService contactService;

    @Autowired
    public ContactRestController(ContactService contactService) {
        this.contactService = contactService;
    }

    @GetMapping
    public Iterable<Contact> contacts(){
        return contactService.getContacts();
    }
}

Az első fontos lépés, hogy a REST API-ban résztevevő osztályra a @RestController annotációt elhelyezzük! Ez az annotáció kettős jelentőséggel bír. Egyrészt ugyanúgy stereotype annotáció, tehát automatikusan regisztrálja a Spring egy beanként és így bárhol hozzáférhető lesz az alkalmazásukban (@Autowired). Másrészt ez az annotáció azt is megmondja, hogy a benne található kezelő metódusok a visszatérési értéküket közvetlenül a válasz body-jába írják (nem pedig egy modellen keresztül cipeljük a view-hoz).

Megjegyzés

A fenti megvalósítás egyenértékű azzal, ha az osztályt a @Controller annotációval látjuk el, de ebben az esetben az összes metódust annotálnunk kell a @ResponseBody-val, mely pontosan a fenti második pontot jelenti.

A @RequestMapping az osztályban megadott összes metódusra ad egy URL prefix-et, melyet tovább pontosíthatunk a metódusra adott Mapping-ekkel (jelen esetben a @GetMapping nem ad hozzá ehhez semmit, tehát a /api/contact URL-re küldött GET kéréseket fogja kiszolgálni). A @RequestMapping-nél azt is megadjuk, hogy csak akkor szolgáljuk ki a kérést, ha annak Accept header-ében megtalálható az application/json. Ezzel igazából limitáljuk, hogy csak JSON eredményeket adunk, ugyanakkor lehet másik controller-ünk ugyanezzel az útvonallal (Pl.: az MVC-s megvalósításból /contact), feltéve, hogy azok a kérések nem igénylik a JSON output-ot (máskülönben összevesznének).

Megjegyzés

Amennyiben JSON mellett mondjuk XML-t is engedélyezni szeretnénk, akkor a produces attribútumban egy listát is megadhatunk:

1
@RequestMapping(path = "/api/contact", produces={"application/json", "text/xml"})

Amennyiben a klienseink más host-on futnak, akkor szükségünk lehet arra, hogy engedélyezzük a Cross Origin-t a megadott host-nak, vagy ha publikus API-t készítünk, akkor mindenkinek adhatunk hozzáférést:

1
2
3
4
@CrossOrigin(origins = "*")
public class ContactRestController {
    ...
}

Ezután nézzük, hogy mi az eredménye a REST API hívásnak! Tesztelhetjük Postman-el, vagy egyszerűen curl-el. Amennyiben alapértelmezett porton futtatjuk az alkalmazásunkat, akkor a következőt kell kiadnunk:

1
curl http://localhost:8080/api/contact
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
    {
        "createdAt": null,
        "lastModifiedAt": null,
        "createdBy": null,
        "lastModifiedBy": null,
        "id": 1,
        "name": "Kiss Béla",
        "phone": null,
        "email": "kiss@bela.com",
        "address": null,
        "birthDate": null,
        "group": null
    },
    {
        "createdAt": null,
        "lastModifiedAt": null,
        "createdBy": null,
        "lastModifiedBy": null,
        "id": 2,
        "name": "Nagy János",
        "phone": null,
        "email": "nagy@janos.com",
        "address": null,
        "birthDate": null,
        "group": null
    }
]

A fenti példában elhelyezett elemekben csak a név és email attribútumokat adtuk meg, így ez teljesen helyes.

Feladat

Készítsünk egy másik végpontot, mely id alapján ad vissza egy contact-ot!

Az alapvető megoldás igen egyszerű:

1
2
3
4
@GetMapping("/{id}")
public Contact contactById(@PathVariable Long id){
    return contactService.getContactById(id);
}

A fentiben semmilyen extra dolog nincs, a @PathVariable-el az URL-ben megadott azonosítót leképezzük a metódus paraméterébe.

Egy problémát az jelent, hogy amikor a megadott id érvénytelen, akkor a getContactById() hívás eredménye null lesz, mely nem pont ideális, hiszen ilyen esetben a kliens egy üres body-t kap HTTP 200-as kóddal, vagyis azt mondjuk, hogy minden sikeres volt, pedig nem is létezik az amit kért. Egy jobb megoldás lehet, ha 404-et adunk vissza. Az előző példát ehhez kicsit átpofozzuk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@GetMapping("/{id}")
public ResponseEntity<Contact> contactById(@PathVariable Long id){
    Contact c = contactService.getContactById(id);

    if(c == null){
        return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    }

    return new ResponseEntity<>(c, HttpStatus.OK);
}

Az új elem, amit használunk a ResponseEntity, mely egy generikus osztály. A generikus paramétere nem más, mint a visszaadni kívánt objektum típusa. A lényeges különbség, hogy így a ResponseEntity rendelkezik egy status-al és a header-öket is meg tudjuk adni. A példában megvizsgáljuk, hogy a visszakapott objektum null-e vagy sem. Igaz esetben 404-es HTTP status-t szeretnénk visszaadni, melyet a következőképpen tudunk megvalósítani:

1
ResponseEntity.status(HttpStatus.NOT_FOUND)

A ResponseEntity statikus metódusainak egy nagy része visszaad egy BodyBuilder objektumot (mint, ahogy a status is). Ezeket azok a metódusok adják vissza, melyek a status-t módosítják valamilyen formában, így aztán a BodyBuilder-rel a válasz body-ját adhatjuk meg. Amikor készen vagyunk akkor a build() alkalmazásával kaphatjuk vissza a legyártott ResponseEntity objektumot. A 404 jelzésére használhattunk volna ezt is:

1
return ResponseEntity.notFound().build();

A statikus metódusokon kívül használhatjuk a konstruktort is, hogy a megfelelő ResponseEntity-t előállítsuk. A legbővebb paraméterlistával megadhatjuk a body-t, a header-t és a status kódot is. Jelen helyzetben viszont elegendő a status-t beállítanunk, illetve a body-ba elhelyezni a lekért Contact-ot.

1
return new ResponseEntity<>(c, HttpStatus.OK);

Próbáljuk ki az alkalmazást, mondjuk a 2-es id-val, melynek eredménye a következő:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "createdAt": null,
    "lastModifiedAt": null,
    "createdBy": null,
    "lastModifiedBy": null,
    "id": 2,
    "name": "Nagy János",
    "phone": null,
    "email": "nagy@janos.com",
    "address": null,
    "birthDate": null,
    "group": null
}

Amennyiben érvénytelen id-val hívjuk a végpontot (Pl.: http://localhost:8080/api/contact/5), akkor 404 - Not Found statust kapunk vissza üres body-val.

Létrehozás

Feladat

A lekérések mellett adjunk lehetőséget arra, hogy új contact-ot adjunk a rendszerhez a REST API-n keresztül!

A feladat nem tűnik túl összetettnek, a következő kód megoldást is ad:

1
2
3
4
5
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Contact addContact(@RequestBody Contact contact){
    return contactService.saveContact(contact);
}

A @PostMapping-ben megadjuk, hogy csakis JSON formátumban vagyunk hajlandóak fogadni az adatot (consumes = "application/json", azaz az ide érkező kéréseknek a Content-Type header-je application/json kell legyen). Továbbá jelen helyzetben a @ResponseStatus(HttpStatus.CREATED)-t használjuk a status beállítására, azaz ha lefut a metódusunk, akkor automatikusan az itt megadott status code-al fog visszatérni a HttpServletResponse-unk.

Ha most kipróbáljuk ezt az alkalmazásunkban, akkor hibát kapunk, pontosabban visszakapunk egy bejelentkezésre felszólító oldalt. Ez a CSRF védelem miatt van, amire adat lekéréskor (korábbi GET kérések) nem kellett figyelnünk, azonban a POST kéréseknél már lehetnek turpisságok, amiket ki kell védenünk. Spring-ben a CSRF védelem alapból be van kapcsolva. Ennek kiküszöbölésére (mivel most nem akarunk semmilyen autentikációt a REST kéréseinkre) hozzá kell nyúlnunk a WebSecurityConfig osztályhoz. A korábban megadott configure(HttpSecurity http) metódust egészítsük ki a legvégén a következővel:

1
2
3
4
5
6
7
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    ...
    .logoutSuccessUrl("/login")
    .and().csrf().ignoringAntMatchers("/api/**");
}

A fenti kódban, mivel nem akarjuk a CSRF-et teljesen kikapcsolni (amit nem is ajánlok), ezért azt mondjuk, hogy csak a /api/** -ra érkező kéréseknél kapcsoljuk ki a CSRF védelmet.

Ha már itt vagyunk, akkor nézzük meg, hogy mi történik, ha a h2-console-t szeretnénk jelenleg elérni. A Security beállításaink miatt be kell jelentkeznünk, pedig magának a h2-console-nak is van bejelentkeztetése. Ha nem szeretnénk, hogy ide is be kelljen jelentkezni, akkor a WebSecurityConfig-ban a következőt is adjuk meg:

1
2
3
4
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/h2-console/**");
}

Ezzel nemes egyszerűséggel azt mondjuk, hogy a /h2-console/ kezdetű URL-eken nem foglalkozunk security-vel.

Frissítés

Feladat

Készítsünk REST API végpontot a Contact frissítéséhez!

Ezzel sem lesz nehéz feladatunk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@PutMapping("/{id}")
public ResponseEntity<Contact> updateContact(@PathVariable Long id, @RequestBody Contact contact){
    if(contact.getId() == null){
        contact.setId(id);
    } else if(!Objects.equals(id, contact.getId())){
        return ResponseEntity.badRequest().build();
    }

    return ResponseEntity.ok(contactService.saveContact(contact));
}

Viszont néhány dolgot azért szem előtt kell tartanunk! Ha nincs megadva a contact id-ja, akkor használjuk az URL-ben megadottat, máskülönben megvizsgáljuk, hogy a két azonosító megegyezett-e, mivel eltérés esetén valami nem úgy lett elküldve, ahogy a felhasználó szerette volna.

Tegyük fel hogy a 3-as id-val létező kontakt nevét szeretném szerkeszteni, így a http://localhost:8080/api/contact/3-ra a következő body-val rendelkező PUT kérést küldöm:

1
2
3
{
    "name": "Beviz Elek Jónás"
} 

A kontakt sikeresen el is lesz mentve. Viszont, ha lekérdezem a kontaktot akkor a következőt kapom:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "createdAt": null,
    "lastModifiedAt": "2020-10-11T15:17:08.916+00:00",
    "createdBy": null,
    "lastModifiedBy": "anonymousUser",
    "id": 3,
    "name": "Beviz Elek Jónás",
    "phone": null,
    "email": null,
    "address": null,
    "birthDate": null,
    "group": null
}

Ez azért történik, mert bár a @RequestBody Contact contact paraméterbe szépen leképződik egy kontakt, azonban ennek a kontakt objektumnak nincs megadva csak a neve, a többi field null lesz és ezt mentjük el az adatbázisba. Tehát, ezen logika alapján, akkor fog jól működni az update, ha a kliens az összes szükséges adatot elküldi.

Amennyiben szeretnénk olyan megoldást, amely nem nullázza ki a meglévő adatot, akkor ajánlatos lehet egy PATCH kérést elkészítése is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@PatchMapping("/{id}")
public ResponseEntity<Contact> patchContact(@PathVariable Long id, @RequestBody Contact contact){

    Contact existing = contactService.getContactById(id);
    if(existing == null){
        return ResponseEntity.notFound().build();
    }

    if(contact.getAddress() != null){
        existing.setAddress(contact.getAddress());
    }

    if(contact.getBirthDate() != null){
        existing.setBirthDate(contact.getBirthDate());
    }

    if(contact.getEmail() != null){
        existing.setEmail(contact.getEmail());
    }

    if(contact.getGroup() != null){
        existing.setGroup(contact.getGroup());
    }

    if(contact.getName() != null){
        existing.setName(contact.getName());
    }

    if(contact.getPhone() != null){
        existing.setPhone(contact.getPhone());
    }

    return ResponseEntity.ok(contactService.saveContact(existing));
}

A fenti kéréssorozatot megismételve most már az elvártat kell kapjuk és csak a name field szabad, hogy módosuljon (group eddig is null volt):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "createdAt": "2020-10-11T15:32:50.871+00:00",
    "lastModifiedAt": "2020-10-11T15:33:04.211+00:00",
    "createdBy": "anonymousUser",
    "lastModifiedBy": "anonymousUser",
    "id": 3,
    "name": "Beviz Elek Jónás",
    "phone": "+36 20 111 2222",
    "email": "elek@beviz.com",
    "address": "9999 Alsóbucsaröcsöge, Jajj utca 2.",
    "birthDate": "1977-12-24T00:00:00.000+00:00",
    "group": null
}

Törlés

Feladat

Végül készítsük el a törléshez szükséges API végpontot is!

1
2
3
4
5
@DeleteMapping("/{id}")
@ResponseStatus(code=HttpStatus.NO_CONTENT)
public void deleteContact(@PathVariable Long id){
    contactService.deleteContact(id);
}

Egy DELETE kéréseket támogató API végpontot definiálunk, melyben töröljük a megadott id-val rendelkező elemet. Ha eleve nincs ilyen id-jú elem, akkor a célunkat végülis elértük, ugyanis nincs ilyen azonosítójú elem az adatbázisban, ezért nem kezeljük külön, hogy mi történjen akkor, ha az elem nem létezik. A @ResponseStatus(code=HttpStatus.NO_CONTENT) a választ a 204-es kóddal látja el, mely szerint sikeres kérést hajtottunk végre, de nincs a válaszban semmi.

Hypermedia engedélyezése

Az eddig megírt API teljesen rendben van és nagyszerűen működik. Viszont a kliensnek teljes egészében ismernie kell az API felépítését. Például a kliensnek tudnia kell, hogy a http://localhost:8080/api/contact URL-en kérheti le az összes Contact-ot, illetve azt is, hogy ha ehhez hozzárakja az id-t, akkor lekérheti az adott Contact tulajdonságait. Ez mindaddig rendben is van ameddig nem változik az API.

Ebben a problémakörben tud segíteni a HATEOAS (Hypermedia as the Engine of Application State), amely önleíró API készítését teszi lehetővé úgy, hogy a visszaadott válaszban linkeket helyez el az adott erőforrások elérésére. Segítségével a kliens magabiztosabban navigálhat az API végpontjain anélkül, hogy pontosan tudnia kéne, hogy milyen végpontot is használ. Ehelyett az API által szolgáltatott (visszaadott) erőforrások közötti kapcsolatokra fókuszál.

Annak érdekében, hogy könnyebben megértsük a fent leírtakat, íme egy példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
  "_embedded" : {
    "contacts" : [ {
      "createdAt" : null,
      "lastModifiedAt" : null,
      "createdBy" : null,
      "lastModifiedBy" : null,
      "name" : "Kiss Béla",
      "phone" : null,
      "email" : "kiss@bela.com",
      "address" : null,
      "birthDate" : null,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api-v2/contacts/1"
        },
        "contact" : {
          "href" : "http://localhost:8080/api-v2/contacts/1"
        },
        "group" : {
          "href" : "http://localhost:8080/api-v2/contacts/1/group"
        }
      }
    }, {
      "createdAt" : null,
      "lastModifiedAt" : null,
      "createdBy" : null,
      "lastModifiedBy" : null,
      "name" : "Nagy János",
      "phone" : null,
      "email" : "nagy@janos.com",
      "address" : null,
      "birthDate" : null,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api-v2/contacts/2"
        },
        "contact" : {
          "href" : "http://localhost:8080/api-v2/contacts/2"
        },
        "group" : {
          "href" : "http://localhost:8080/api-v2/contacts/2/group"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api-v2/contacts"
    },
    "profile" : {
      "href" : "http://localhost:8080/api-v2/profile/contacts"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 2,
    "totalPages" : 1,
    "number" : 0
  }
}

A HATEOAS maga a technika, melynek több megvalósítása is létezik. A fent látható formátum a HAL (Hypertext Application Language).

A listában minden elem rendelkezik egy _links property-vel, mely olyan hyperlink-eket tárol a kliens számára, amelyeken keresztül navigálhat. Például a Kiss Béla kontaktnál a self megmondja, hogy erről a kontaktról, hol tudunk lekérdezni infokat (ezt a típusával is biztosítja számunkra), továbbá láthatjuk, hogy a kontakthoz tartozó csoportot melyik URL-en kérhetem le.

A fentiek azért nagyon jók, mert a kliensnek nem kell ismernie az API felépítését, nem kell az URL-ekkel machinálnia, egyszerűen a megfelelő linkeket kell követnie, amelyeket a szerver állított elő a számára.

A Spring alkalmazásunkhoz a következő függőséget kell hozzáadnunk, ha szeretnénk használni a HATEOAS nyújtotta lehetőségeket:

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

Azon felül, hogy ez így bekerül a classpath-ba, ad egy autokonfigurációt is, amely miatt csak a kontrollereket kell kicsit átínunk, hogy használni tudjuk a HATEOAS-t.

Ezután a contacts() metódust írjuk át a következőképpen:

1
2
3
4
@GetMapping
public CollectionModel<EntityModel<Contact>> contacts(){
    return CollectionModel.wrap(contactService.getContacts());
}

A fenti példában két új elemmel találkozhatunk: CollectionModel és EntityModel, melyek rendre egy kollekció és maga egy entitás HATEOAS megfelelői. A CollectionModel wrap metódusát meghívva becsomagolhatjuk a visszakapott kontaktok listáját egy CollectionModel<EntityModel<Contact>> típusba.

Próbáljuk is ki, hogy mit kapunk eredményül:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
    "_embedded": {
        "contactList": [
            {
                "createdAt": null,
                "lastModifiedAt": null,
                "createdBy": null,
                "lastModifiedBy": null,
                "id": 1,
                "name": "Kiss Béla",
                "phone": null,
                "email": "kiss@bela.com",
                "address": null,
                "birthDate": null,
                "group": null
            },
            ...
        ]
    }
}

Az eddigiekhez képest annyi a különbség, hogy az egész válasz, mely eddig egy lista volt, most belekerült egy objektumba, melynek van egy _embedded property-je, amin belül a contactList adja meg magukat a kontaktokat.

Adjunk hozzá linkeket is!

1
2
3
4
5
6
@GetMapping
public CollectionModel<EntityModel<Contact>> contacts(){
    CollectionModel<EntityModel<Contact>> contactsModel = CollectionModel.wrap(contactService.getContacts());
    contactsModel.add(Link.of("http://localhost:8080/api/contact", "self"));
    return contactsModel;
}
A CollectionModel-hez az add(...) metódussal tudunk egy új linket hozzáadni, melynek eredményeképpen a JSON végére bekerül a következő:

1
2
3
4
5
"_links": {
        "self": {
            "href": "http://localhost:8080/api/contact"
        }
    }

Ez az egyik kulcsa annak, hogy elkészíthessük a HATEOAS támogatással bíró válaszokat. A linkek létrehozásakor a relation type-ot elhagyhatjuk, ha self típusú linket szeretnénk létrehozni (mivel ez az alapértelmezett), így a fentit írhattuk volna így is:

1
contactsModel.add(Link.of("http://localhost:8080/api/contact"));

Kezdetnek nem is rossz, de ez csak a teljes lista elérését adja meg a válaszban, nincs link magukra az egyes kontaktokra, illetve a kontaktokhoz tartozó group-okhoz sem tartozik link.

Vegyük észre, hogy mennyire nem jó ötlet az URL-t hardcode-olni!

Ennek a problémának a leküzdésében az úgynevezett link builder-ek lesznek a segítségünkre. A konkrét osztály, mely segít nekünk a WebMvcLinkBuilder, melynek használata a következőképpen nézhet ki:

1
2
3
4
5
6
7
@GetMapping
public CollectionModel<EntityModel<Contact>> contacts(){
    CollectionModel<EntityModel<Contact>> contactsModel = CollectionModel.wrap(contactService.getContacts());
    contactsModel.add(WebMvcLinkBuilder.linkTo(ContactRestController.class)
            .withRel("contact"));
    return contactsModel;
}

Ennek következtében a szerver felépíti az URL-t, nekünk csak a .withRel("contact")-t kell megadnunk. Az útvonal többi részét a linkTo() paraméterében megadott ContactRestController osztályra elhelyezett URL-ből számítja a rendszer.

Amennyiben további URL részeket szeretnénk megadni, akkor használhatjuk a slash("asd") metódust is, mely egy / jellel hozzáfűzi az eddigi URL-hez azt, amit a paraméterben megadunk. Ez akkor jöhet jól, ha nem csak a controller basepath-t használjuk, hanem az adott kezelő metóduson van még valamilyen URL megadva a mapping-ben. Ilyenkor azonban a slash helyett használhatunk egy másik alternatívát, mely a kezelő metódus mapping-jét is figyelembe veszi:

1
2
3
contactsModel.add(
        linkTo(methodOn(ContactRestController.class).contacts()).withSelfRel()
);

Most hogy ez megvan lássuk, hogy hogyan lehet a kontaktokhoz és a csoportokhoz is hozzáadni a linkeket. A fenti tudásunk alapján csinálhatnánk azt, hogy egyszerűen végig iterálunk a lista összes elemén és hozzáadogatjuk a linkeket, ahogy azt már láttuk is. Ezt viszont elég sok helyen meg kellene ismételnünk, ami nem túl hatékony.

Egy megoldás erre a RepresentationModel<T> használata, melyből származtatni kell az adott modellt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package hu.suaf.contacts.model.hateoas;

import hu.suaf.contacts.model.ContactGroup;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.hateoas.RepresentationModel;

import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
public class ContactResource extends RepresentationModel<ContactResource> {

    private String name;
    private String phone;
    private String email;
    private String address;
    private Date birthDate;
    private GroupResource group;
    private Date createdAt;
    private Date lastModifiedAt;
    private String createdBy;
    private String lastModifiedBy;

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package hu.suaf.contacts.model.hateoas;

import hu.suaf.contacts.model.ContactGroup;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;

import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
public class GroupResource extends RepresentationModel<GroupResource> {

    private String name;
    private Date createdAt;
    private Date lastModifiedAt;
    private String createdBy;
    private String lastModifiedBy;
}

Ezen felül szükségünk lesz egy-egy segédosztályra is, amelyek az alap modell osztályaink és a Resource osztályaink közötti átalakításban fognak segíteni nekünk. Ezeket az osztályokat a RepresentationModelAssemblerSupport<ModelT, ResourceT> generikus oszályból való származtatással fogjuk megvalósítani:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package hu.suaf.contacts.model.hateoas.assemblers;

import hu.suaf.contacts.model.Contact;
import hu.suaf.contacts.model.hateoas.ContactResource;
import hu.suaf.contacts.rest.ContactRestController;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;

public class ContactResourceAssembler extends RepresentationModelAssemblerSupport<Contact, ContactResource> {

    public ContactResourceAssembler() {
        super(ContactRestController.class, ContactResource.class);
    }

    @Override
    public ContactResource toModel(Contact entity) {
        return createModelWithId(entity.getId(), entity);
    }

    @Override
    protected ContactResource instantiateModel(Contact entity) {
        ContactResource resource = new ContactResource();
        resource.setAddress(entity.getAddress());
        resource.setBirthDate(entity.getBirthDate());
        resource.setCreatedAt(entity.getCreatedAt());
        resource.setEmail(entity.getEmail());
        resource.setGroup(new GroupResourceAssembler().instantiateModel(entity.getGroup()));
        resource.setLastModifiedAt(entity.getLastModifiedAt());
        resource.setLastModifiedBy(entity.getLastModifiedBy());
        resource.setName(entity.getName());
        resource.setPhone(entity.getPhone());
        return resource;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package hu.suaf.contacts.model.hateoas.assemblers;

import hu.suaf.contacts.model.ContactGroup;
import hu.suaf.contacts.model.hateoas.GroupResource;
import hu.suaf.contacts.rest.GroupRestController;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

public class GroupResourceAssembler extends RepresentationModelAssemblerSupport<ContactGroup, GroupResource> {

    public GroupResourceAssembler() {
        super(GroupRestController.class, GroupResource.class);
    }

    @Override
    public GroupResource toModel(ContactGroup entity) {
        return instantiateModel(entity);
    }

    @Override
    protected GroupResource instantiateModel(ContactGroup entity) {
        GroupResource resource = new GroupResource();
        resource.setCreatedAt(entity.getCreatedAt());
        resource.setCreatedBy(entity.getCreatedBy());
        resource.setLastModifiedAt(entity.getLastModifiedAt());
        resource.setLastModifiedBy(entity.getLastModifiedBy());
        resource.setName(entity.getName());
        resource.add(linkTo(methodOn(GroupRestController.class).getGroupById(entity.getId())).withSelfRel());
        return resource;
    }
}

A fentiek fényében a ContactRestController-ünk listázás része a következőképpen fog módosulni:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping
public CollectionModel<ContactResource> contacts(){
    List<Contact> contacts = contactService.getContacts();

    CollectionModel<ContactResource> contactResources = new ContactResourceAssembler().toCollectionModel(contacts);

    contactResources.add(
            linkTo(methodOn(ContactRestController.class).contacts()).withSelfRel()
    );
    return contactResources;
}

Videó

A gyakorlat anyagáról készült videó:

SUAF_06_gyak

Linkek


Utolsó frissítés: 2021-10-20 21:11:15
Back to top