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ń algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 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ń algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.

Kategorie: ,

Ostatnia aktualizacja:

Autor: Marcin Pietraszek


Nie popełnia błędów tylko ten, kto nic nie robi ;). Bardzo możliwe, że znajdziesz błąd, literówkę, coś co wymaga poprawy. Jeśli chcesz możesz samodzielnie poprawić tę stronę. Jeśli nie chcesz poprawiać błędu, który udało Ci się znaleźć będę wdzięczny jeśli go zgłosisz. Z góry dziękuję!

Zostaw komentarz