Projekt Informator

Projekt informator to REST’owy web service, działający w oparciu o Spring i Hibernate. Jeśli chcesz przeczytać więcej o projekcie i jego założeniach zapraszam do wprowadzenia.

W jednym z poprzednich artykułów przeczytasz też o wdrożeniu projektu w chmurze.

Samouczek Programisty jest jednym z partnerów konferencji infoShare 2018.

infoShare 2018 to konferencja technologiczna odbywająca się 22-23 maja w Gdańsku. Na developerów czekają m.in. prelekcje z obszaru cybersecurity i machine learning, live coding oraz spotkania ze specjalistami, takimi jak: Filip Wolski, Trent McConaghy, Piotr Konieczny, Zbigniew Wojna czy Scott Helme. infoShare to także okazja do networkingu i udziału w imprezach towarzyszących. Sprawdź agendę i zarejestruj się na www.infoshare.pl.

Baza danych

W projekcie do mapowania obiektowo relacyjnego używam biblioteki Hibernate jako implementacji JPA (ang. Java Persistence API). W tym przypadku tworzenie schematu bazy danych zostawiam JPA. Poniżej widzisz konfigurację obiektu zarządzanego przez kontener Spring’a. Służy on do tworzenia instancji implementującej interfejs EntityManager:

@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    factory.setPackagesToScan("pl.samouczekprogramisty.informator.model");
    factory.setDataSource(dataSource());

    Properties jpaProperties = new Properties();
    jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
    jpaProperties.setProperty("hibernate.show_sql", "true");
    jpaProperties.setProperty("hibernate.format_sql", "true");
    jpaProperties.setProperty("hibernate.hbm2ddl.auto", "validate");
    // create database schema if missing
    jpaProperties.setProperty("javax.persistence.schema-generation.database.action", "create");
    factory.setJpaProperties(jpaProperties);

    return factory;
}

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 1000 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Zasilenie bazy danych

Niestety organizatorzy konferencji nie przygotowali źródła danych, które w łatwy sposób można użyć do zasilenia bazy danych. Jedyne źródło to oficjalna strona www konferencji. Na początku skupiłem się nad zasileniem tabeli zawierającej dane dotyczące prelegentów. W projekcie Informator prelegent reprezentowany jest przez instancję klasy Speaker:

@Entity
public class Speaker {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "speaker_seq")
    @SequenceGenerator(name = "speaker_seq")
    private Integer id;

    private Integer infoshareId;

    private Category category;

    private String name;

    private URL linkedinProfile;
    private URL twitterProfile;
    private URL facebookProfile;
    private URL githubProfile;

    @Column(columnDefinition = "text")
    private String description;

    // getters/setters
}

Analizując zapytania HTTP, które są wykonywane w tle zauważyłem adres w postaci:

https://infoshare.pl/speaker2.php?cid=48&id=XXX&year=2018&agenda_id=99999&fancybox=true

W adresie tym XXX zastąpione jest identyfikatorem prelegenta. Strona z prelegentami zawiera listę wszystkich osób występujących na każdej ze scen. Żeby wyciągnąć informacje o wszystkich prelegentach potrzeba ponad 200 zapytań.

Z racji tego, że jest to dość żmudne i czasochłonne zadanie napisałem skrypt1, który wyciąga niezbędne dane. W wyniku działania tego skryptu powstał plik speakers.sql. Wewnątrz tego pliku znajdują się instrukcje SQL (ang. Structured Query Language), które dodają wiersze do tabeli speaker. Przykładowe zapytanie z tego pliku wygląda następująco:

INSERT INTO speaker (
	id,
	infoshareid,
	category,
	description,
	facebookprofile,
	githubprofile,
	linkedinprofile,
	twitterprofile,
	name
)
VALUES (
	nextval('speaker_seq'),
	954,
	0, 'Stephen Haunts is a veteran sof(...)',
	NULL,
	NULL,
	NULL,
	'https://twitter.com/stephenhaunts',
	'Stephen Haunt'
);

Formatowanie odpowiedzi

Mając rzeczywiste dane w bazie danych webservice może odpowiadać bardziej sensownymi danymi:

$ curl http://localhost:8080/speakers/7 -s | json_pp
{
   "category" : "STARTUP",
   "description" : "Kamila Wincenciak is a member of Ali(...)",
   "name" : "Kamila Wincenciak",
   "githubProfile" : null,
   "twitterProfile" : null,
   "facebookProfile" : null,
   "linkedinProfile" : "https://www.linkedin.com/in/kamila-wincenciak-27560130/"
}

Zabrałem się za kolejny etap, czyli obsługę błędów. Przypadkami, które trzeba obsłużyć są brak rekordu w bazie i złe dane wprowadzone przez użytkownika. Oba przypadki pokazane są poniżej. Proszę zwróć uwagę na zwracane nagłówki i status odpowiedzi:

$ curl http://localhost:8080/speakers/-1 -vs | json_pp
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /speakers/-1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 404 
< Content-Type: application/json
< Content-Length: 148
< Date: Wed, 20 Jun 2018 21:09:42 GMT
< 
{ [148 bytes data]
* Connection #0 to host localhost left intact
{
   "responseCode" : 404,
   "exceptionClass" : "pl.samouczekprogramisty.informator.exceptions.NotFoundException",
   "message" : "Speaker with id -1 wasn't found!"
}


$ curl http://localhost:8080/speakers/aa -vs | json_pp
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /speakers/aa HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 400 
< Content-Type: application/json
< Content-Length: 108
< Date: Wed, 20 Jun 2018 21:09:16 GMT
< Connection: close
< 
{ [108 bytes data]
* Closing connection 0
{
   "message" : "For input string: \"aa\"",
   "responseCode" : 400,
   "exceptionClass" : "java.lang.NumberFormatException"
}

Konfiguracja Spring a obsługa błędów

Aby móc w ten sposób formatować błędy użyłem kombinacji adnotacji ControllerAdvice i ExceptionHandler:

@ControllerAdvice
@SuppressWarnings("unused")
@ResponseBody
public class InformatorExceptionHandler {

    private static ObjectMapper mapper = new ObjectMapper();

    public static class ErrorResponse {
        private static final MultiValueMap<String, String> HEADERS = new LinkedMultiValueMap<>(
                Collections.singletonMap(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE))
        );
        private final Exception exception;
        private HttpStatus responseStatus;

        ErrorResponse(HttpStatus responseStatus, Exception exception) {
            this.exception = exception;
            this.responseStatus = responseStatus;
        }

        ResponseEntity<String> buildResponse() {
            try {
                return new ResponseEntity<>(mapper.writeValueAsString(this), HEADERS, responseStatus);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }

        // getters
    }

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<String> handleNotFound(NotFoundException exception) {
        return new ErrorResponse(HttpStatus.NOT_FOUND, exception).buildResponse();
    }

    @ExceptionHandler(NumberFormatException.class)
    public ResponseEntity<String> handleNumberFormat(NumberFormatException exception) {
        return new ErrorResponse(HttpStatus.BAD_REQUEST, exception).buildResponse();
    }
}

Klasa oznaczona adnotacją ControllerAdvice zawiera w sobie metody, które są użyte w wielu kontrolerach. Możemy powiedzieć, że są to metody przekrojowe. Przykładem takich metod są te oznaczone adnotacją ExceptionHandler. Każda z nich odpowiada za obsługę innego typu wyjątku.

Niestety w tym przypadku Spring nie deserializuje obiektu odpowiedzi do żądanego formatu dlatego napisałem klasę pomocniczą ErrorResponse, która przygotowuje odpowiedź w formacie JSON.

Podsumowanie

Aplikacja aktualnie jest w stanie wyświetlić informacje o prelegencie na podstawie rzeczywistych danych pobranych ze strony organizatora konferencji. Dodatkowo aplikacja poprawnie reaguje na różnego rodzaju błędy odpowiadając w formacie JSON. Zachęcam Cię do przeanalizowania kodu źródłowego aplikacji, w ten sposób utrwalisz zdobytą wiedzę.

Po przeczytaniu tego artykułu i przejrzeniu kodu źródłowego wiesz w jaki sposób można obsługiwać błędy w webservice’ach. Poznałeś też sposób na zasilanie bazy danych na podstawie informacji umieszczonych na innych stronach.

Jeśli nie chcesz pominąć kolejnych artykułów na Samouczku proszę dopisz się do samouczkowego newslettera i polub Samouczka na Facebooku. Proszę podziel się linkiem do artykułu ze znajomymi, którym może on pomóc. Może to dzięki Tobie uda mi się dotrzeć do nowych czytelników? ;)

Do następnego razu!

  1. Po godzinach pracy, w wolnym czasie uczę się języka Go. Wiem, że najlepszy sposób na naukę to praktyka. Dlatego właśnie napisałem ten skrypt używając tego języka. Mam świadomość, że nie jest idealny i wymaga sporo poprawek, ale jak na początek nauki jest OK ;). 

Pobierz opracowania zadań z rozmów kwalifikacyjnych

Przygotowałem rozwiązania kilku zadań z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 1000 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Zostaw komentarz