Bazy danychJavaLive

Jakie zadanie rekrutacyjne może Cię spotkać na rozmowie rekrutacyjnej?

Jakiś czas temu na kanale opublikowałem film, który cieszy się ogromną popularnością, a tematem było zadanie rekrutacyjne jakie może Cię spotkać na stanowisku Java Developer. Spośród 4 zadań publiczność wybrała jedno jakie zostało zrealizowane w trakcie LiveStream.
W tym artykule pokażę Ci jak krok po kroku należy rozwiązać takie zadanie.
Ponieważ w trakcie LiveStramu dysponowałem ograniczoną ilością czasu, to skupiłem się na logice pomijając rzeczy, o których trzeba pamiętać:

  1. testy
  2. logowanie
  3. obsługa wyjątków
  4. zapisywanie stałych w polach interfejsu

Zostawiam Ci te rzeczy do zrobienia jako praca domowa 😉

Spis treści

  1. Treść zadania
  2. Przygotowanie projektu
  3. Kod na GitHubie
  4. Treść zadania
  5. Autoryzacja
  6. Tworzenie użytkowników
  7. Upload zdjęć
  8. Interfejs użytkownika
  9. Rezultat
  10. Zostańmy w kontakcie
  11. Kod do GitHub
  12. Video

Treść zadania

Hosting zdjęć. Aplikacja posiada dwa rodzaje użytkowników, których przechowuje w bazie danych – animistrator, oraz user. Po zalogowaniu administrator ma możliwość dodawanie zdjęć. Zalogowany użytkownik przegląda dostępne galerie. Użytkownik niezalogowany ma dostępną formatkę logowania. ~Nadesłane przez Karola.

Inne przykładowe zadania są widoczne w materiale video.

Przygotowanie projektu

Projekt został stworzony z wykorzystaniem Spring Boot. Zależności dodane do projektu:

  • Spring Web Starter – umożliwa towrzenie aplikacji webowej
  • Vaadin – biblioteka do tworzenia GUI
  • Spring Security – zestaw narzędzi umożliwiających na tworzenie zabezpieczeń aplikacji przed niepowołanym dostępem
  • Spring Data JPA – zestaw narzędzi pozwalających na komunikowanie się z relacyjną bazą danych
  • MySQL Driver – sterownik umożliwiający na łączenie się z bazą danych MySQL

Autoryzacja

Kluczową funkcjonalnością w projekcie jest autoryzacja. Dlatego trzeba stworzyć klasę pozwalającą na zarządzanie prawami użytkowników. Utwórz klasę WebSeciurtiyConfig, która będzie dziedziczyła po WebSecurityConfigurerAdapter.

Wewnątrz niej trzeba zaimplementować metodę configure(HttpSecurity http). Odpowiada ona za autoryzację. Należy nadać użytkownikowi USER prawa do endpointu /gallery, oraz użtownikowi ADMIN dostęp do endpointu /upload. W późniejszym etapie endpointy te będą udostępniały usługi kolejno przeglądania zdjęć i umieszczania zdjęć na serwerze.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/upload").hasRole("ADMIN")
                .antMatchers("/gallery").hasRole("USER")
                .and()
                .formLogin().permitAll()
                .and()
                .csrf().disable();
    }

Dodatkowo w kodzie został wyłączony CSRF – po to, aby było możliwe wykorzystania biblioteki Vaadin.

Tworzenie użytkowników

Musimy utworzyć model dla użytkownika. Aby był on zgodny z interfejsem wymaganym przez Spring Security do obsługi autoryzacji to musi on dodatkowo implementować UserDetails.

@Entity
public class AppUser implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role;

    public AppUser(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public AppUser() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority(role));
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Musimy uwzględnić też repozytorium, które umożliwi nam na zapis użytkownika do bazy danych. W repozytorium należy dodać metodę pozwalającą na zlokalizowanie użytkownika po jego nazwie.

@Repository
public interface AppUserRepo extends extends JpaRepository<AppUser, Long>  {

    AppUser findByUsername(String username);
}

Aby użytkownika można było autoryzować z wykorzystaniem Spring Security musimy zaimplementować interfejs UserDetailsService. Następnie w klasie implementującej przekazujemy użytkownika z repozytorium z wykorzystaniem metody loadUserByUsername

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private AppUserRepo appUserRepo;

    @Autowired
    public UserDetailsServiceImpl(AppUserRepo appUserRepo) {
        this.appUserRepo = appUserRepo;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return appUserRepo.findByUsername(s);
    }
}

Do pełni autoryzacji wystawczy podpiąć implementacje do klasy WebSeciurtiyConfig:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

Upload zdjęć

Do uploadu zdjęć posłużyłem się serwisem Cloudinary. Serwis ten umożliwia na darmowy upload plików multimedialnych. Aby z niego skorzystać należy dodać zależność do projektu:

<dependency>
	<groupId>com.cloudinary</groupId>
	<artifactId>cloudinary-http44</artifactId>
	<version>1.22.1</version>
</dependency>

Po dokonanej rejestracji w serwisie dostajemy Cloud name, API Key i API Secret. Dane te umieszczamy w pliku application.properties, natomiast w nowej klasie podstaw te wartości i utwórz metodę, która na podstawie ścieżki umieści plik na serwerze

@Service
public class ImageUpader {

    private Cloudinary cloudinary;
    private ImageRepo imageRepo;

    @Value("${cloudNameValue}")
    private String cloudNameValue;
    @Value("${apiKeyValue}")
    private String apiKeyValue;
    @Value("${apiSecretValue}")
    private String apiSecretValue;

    @Autowired
    public ImageUpader(ImageRepo imageRepo,
                       @Value("${cloudNameValue}") String cloudNameValue,
                       @Value("${apiKeyValue}") String apiKeyValue,
                       @Value("${apiSecretValue}") String apiSecretValue) {
        this.imageRepo=imageRepo;
        cloudinary = new Cloudinary(ObjectUtils.asMap(
                "cloud_name", cloudNameValue,
                "api_key", apiKeyValue,
                "api_secret", apiSecretValue));
    }

    public String uploadFileAndSaveToDb(String path) {
        File file = new File(path);
        Map uploadResult = null;
        try {
            uploadResult = cloudinary.uploader().upload(file, ObjectUtils.emptyMap());
            imageRepo.save(new Image(uploadResult.get("url").toString()));
        } catch (IOException e) {
            // todo
        }
        return uploadResult.get("url").toString();
    }
}

Opcjonalnie możesz stworzyć Obiekt Image i ImageRepo (tak jak ja to zrobiłem), które umożliwią zapis i odczyt obrazka z bazy danych.

@Entity
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String imageAdress;

    public Image(String imageAdress) {
        this.imageAdress = imageAdress;
    }

    public Image() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getImageAdress() {
        return imageAdress;
    }

    public void setImageAdress(String imageAdress) {
        this.imageAdress = imageAdress;
    }
}
@Repository
public interface ImageRepo extends JpaRepository<Image, Long> {}

GUI

Ostatnim etapem jest przygotowanie GUI, które umożliwi zapis i odczyt danych obrazków. Tu jest pełna dobrowolność. Ja moja propozycja wygląda następująco:


@Route("gallery")
public class GalleryGui extends VerticalLayout {

    private ImageRepo imageUpader;

    @Autowired
    public GalleryGui(ImageRepo imageUpader) {
        this.imageUpader = imageUpader;
        List<Image> all = imageUpader.findAll();
        all.stream().forEach(element -> {
            com.vaadin.flow.component.html.Image image =
                    new com.vaadin.flow.component.html.Image(element.getImageAdress(), "brak");
            add(image);
        } );
    }
}

@Route("upload")
public class UploadGui extends VerticalLayout {

    private ImageUpader imageUpader;

    @Autowired
    public UploadGui(ImageUpader imageUpader) {
        this.imageUpader = imageUpader;

        Label label = new Label();
        TextField textField = new TextField();
        Button button = new Button("upload");
        button.addClickListener(clickEvent -&gt;
        {
            String uploadedImage = imageUpader.uploadFileAndSaveToDb(textField.getValue());
            Image image = new Image(uploadedImage, "nie ma obrazka :(");
            label.setText("Udało się wrzucić obrazek!!!!!!!!");
            add(label);
            add(image);

        });

        add(textField);
        add(button);
    }
}

Rezultat

Tak wygląda aplikacja wczytująca galerie użytkownika.


Zostańmy w kontakcie

Jeśli chcesz być powiadamiany o treściach dotyczących tworzenia aplikacji z wykorzystaniem Spring to zostaw mi do siebie kontakt 😉

Kod do GitHub

https://github.com/bykowski/springboot-image-uploader

Video

Wolisz posłuchać i zobaczyć krok po kroku co trzeba zrobić, aby napisać taką aplikacje? Obejrzyj wideo!

 

Tags:
Show Buttons
Hide Buttons