https://visualhunt.com/photo/196654/

Pierwsze kroki z Play Framework

Po tygodniowej przerwie od pisania na temat Sterownika domowego (DSP2017) pora wrócić do meritum. Oczywiście ostatnie 7 dni nie minęło zupełnie bezproduktywne, co potwierdza kilka nowych commitów na GitHub. Tej pracy niestety nie było tyle, ile bym chciał, więc stwierdziłem, że muszę zmienić proporcje czasu poświęcanego na tworzenie projektu i jego opisywanie. Nie jestem wprawionym blogerem, co skutkuje tym, że nie idzie mi to sprawnie. Tyle moich przemyśleń (ale chyba mogę sobie na nie pozwolić, bo to w końcu blog), pora przejść do konkretów.

Sprzętowe środowisko testowe dla mojego projektu na razie odłożyłem na bok. Do tej pory podłączyłem wszystkie elementy, którymi chcę sterować i napisałem proste skrypty testujące ich działanie. W związku z tym, że wszystko przebiegło pomyślnie (sprzęt działa), poprzestanę na tym etapie. Na pewno wrócę do tematu za 2-3 posty, gdy będę chciał zapisywać zmierzone wartości temperatury i wilgotności z czujników oraz stany pozostałych sensorów w bazie danych.

Pora na aplikację webową, czyli część tego projektu, na której najbardziej mi zależy. O technologii jaką wybrałem (Play Framework) nie będę się rozpisywał, odsyłam do literatury. A dlaczego wybrałem akurat ten framework? Bo jest opisywany jako prosty, lekki, wprowadzone zmiany są widoczne w aplikacji od razu (bez ponownego deploya). Ponadto chciałem wrócić do tematu sprzed jakiegoś czasu, kiedy stawiałem pierwsze kroki w aplikacjach webowych, właśnie w Play (tylko kilka wersji wstecz).

Play Framework jest całkiem niezłe wspierany przez różne IDE. Jest Eclipse, IntelliJ oraz NetBeans, w którym to będę pracował. Play umożliwia rozpoczęcie pracy na kilka sposobów. Można wykorzystać narzędzie sbt i utworzyć projekt z szablonu, można wykorzystać przykładowe projekty lub pobrać Activator.

Po rozpakowaniu i dodaniu do classpath zająłem się utworzeniem projektu. Dla przypomnienia:

export PATH=$PATH:/home/maciek/Install/activator-dist-1.3.12/bin
chmod a+x /home/maciek/Install/activator-dist-1.3.12/bin/activator

Dostępny jest plugin (Pleasure Play Framework Plugin) wspierający pracę z Play Framework z poziomu NetBeans, który jest kompatybilny z wersją 8.2. Do tej pory pracowałem na wersji 8.0.2, jednak migracja do 8.2 nie zajęła dużo czasu. Aktualizacje w końcu trzeba robić! Zmiany wymagał jedynie plik konfiguracyjny netbeans.conf, a dokładniej wartość netbeans_jdkhome, gdzie domyślnie znajdowała się nieprawidłowa ścieżka.

Nieprawidłowa ścieżka netbeans_jdkhome
Komunikat informujący o błędnej ścieżce netbeans_jdkhome
netbeans_jdkhome="/usr/lib/jvm/java-8-oracle"

Od tej chwili samo utworzenie projektu to tylko kilka kroków: File->New Project->Play->Play Framework App.

Wybór rodzaju projektu
Wybór rodzaju projektu

W zasadzie wszystko przebiegło automatycznie, poprawy wymagała ścieżka wskazująca na Activator znajdujący się u mnie w systemie. Prawidłowa lokalizacja w moim przypadku:

/home/maciek/Install/activator-dist-1.3.12/bin
Błąd lokalizacji aktywatora
Błąd lokalizacji aktywatora

Właśnie na tym etapie robi się coraz trudniej, bo rozpoczynam poznawanie nowej technologii. Trudniej, ale również bardziej ekscytująco. Pierwsze na co zwróciłem uwagę w pustym projekcie to jego struktura. Ten drobiazg jest istotny w kwestii poruszania się po plikach, abym wiedział gdzie mogę coś zmienić, dodać czy skonfigurować. Struktura wygląda mniej więcej tak:

  • /app/controllers/ – kontrolery zawierające możliwe do wykonania metody, odzwierciedlające wszelkie akcje (czynności)
  • /app/models/ – klasy opisujące model danych
  • /app/views/ – szablony widoków dostępnych dla użytkownika, pliki *.scala.html
  • /conf/application.conf – główny plik konfiguracyjny aplikacji, w którym ustawiam m. in. konfigurację bazy danych, język aplikacji, nagłówki bezpiczeństwa czy konfigurację HTTP (np. konfiguracja Routera). Co dla mnie ważne, plik ten ma bardzo wysokie pokrycie w komentarzach, co z pewnością ułatwi mi pracę.
  • /conf/routes – w pliku określono jakie akcje (w sensie jakie metody jakich kontrolerów) mają być wykonywane w następstwie zapytania HTTP
  • /Important Files/build.sbt – ustawienia Scala Build Tool (SBT) czyli narzędzia do budowania projektu. Do tego miejsca najczęściej będę zaglądał w sytuacji, gdy będę chciał dodać do projektu zależność zewnętrzną, a taką znajdę na mvnrepository.com, gdzie mam podane na tacy co dokładnie mam przekopiować do build.sbt.
    mvnrepository.com
    Korzystam z tego samego narzędzia jak w projekcie Mavenowym
  • /Important Files/plugins.sbt – plik konfiguracyjny, w którym będę rejestrował pluginy, np. sbt-plugin czy sbt-less.

Zazwyczaj te zadania, które są dla mnie najmniej przyjemne lub najtrudniejsze odkładam na koniec, co jest w zasadzie normalne, bo człowiek wybiera drogę po najmniejszej linii oporu. Tym razem zdecydowałem się postąpić inaczej. Jako iż nigdy nie miewam mitycznego flow jeśli chodzi o frontend, pierwsze co zrobiłem to utworzyłem prosty szablon z wykorzystaniem Bootstrap. Szablon domyślny wygląda następująco:

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
        <link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
        <script src="@routes.Assets.at("javascripts/hello.js")" type="text/javascript"></script>
    </head>
    <body>
        @content
    </body>
</html>

Powyżej widzimy prostą strukturę dokumentu HTML z kilkoma dziwnymi wstawkami. Na początku pliku określamy argumenty, czyli co chcemy przekazać z kontrolera do widoku. W tym schemacie przekazujemy, a następnie uzupełniamy tytuł (<title>@title</title>) oraz @content czyli właściwą zawartość wybranej podstrony. Z grubsza wygląda to podobnie w moim finalnym szablonie, który znajduje się w main.scala.html. Uzupełniłem o menu główne oraz wstawiłem kilka odwołań do plików js (w tym jQuery) oraz css. Całość prezentuje się w przeglądarce następująco:

Wygląd szablonu projektu
Podstawowy szablon dla projektu

Na rozruch zaplanowałem następujące widoki:

  • Plan mieszkania
  • Panel informacyjny
  • Konfiguracja / Sensory (Lista, Dodaj nowy, Rozmieść)
  • Konfiguracja / Alarmy (Lista, Dodaj nowy)
  • Pomoc
  • Profil użytkownika

Przejdę teraz do nieco ciekawszej, jednak dalej podstawowej tematyki. Na etapie „Hello World” utworzyłem kilka modeli danych:

  • Sensor – model danych reprezentujący czujnik/element wykonawczy w systemie
  • Room – model danych reprezentujący pomieszczenie w mieszkaniu
  • Measurement – model danych reprezentujący pomiar (np. z czujnika wilgotności czy temperatury)
  • ElementOutputState – model danych reprezentujący stan czujnika wejściowego, np. kontaktronu czy czujnika PIR

Modele danych to klasy, z których zrobię encje służące w procesie mapowania ORM. Dodam w nich również adnotacje ustawiające parametry kolumn w tabelach bazy danych czy określające ograniczenia (walidacja). W modelach może występować dużo tzn. boilerplate code, czyli czegoś co musimy napisać, a w zasadzie mało co to wnosi do projektu. Objawia się to w postaci setterów i getterów. Oczywiście każde szanujące się IDE wygeneruje te metody za nas, jednak dalej będą one widoczne w pliku, co może pogarszać jego czytelność. Początkowo szukałem w internecie informacji jak zintegrować Play Framework z Lombok, który wcześniej używałem do tego typu zadań. Play jednak oferuje wbudowany plugin Enhancer, który to wygeneruje za mnie akcesory do pól. Nie zrobi tego jednak bezwarunkowo. Dokumentacja mówi, że pola muszą być publiczne, nie mogą być statyczne i finalne. Jeśli chodzi o modyfikator dostępu public, do tej pory ustawiałem pola jako private, ale cóż – dokumentacja Play tak mówi, więc chyba tak się robi. Dodanie pluginu Enhancer to dodanie jednej linijki do pliku plugins.sbt:

addSbtPlugin("com.typesafe.sbt" % "sbt-play-enhancer" % "1.1.0")

W zasadzie to nie pamiętam na 100%, ale chyba był on aktywny domyślnie. Oczywiście istnieje możliwość wyłączenia pluginu w projekcie, jak również pozostawienie go aktywnego, ale wyłączenie generowania setterów i getterów. Po szczegóły odsyłam do dokumentacji

Jeśli mowa o modelu, to trochę czasu zajęło mi wyszukanie jak to jest z jego klasą nadrzędną. Dla EBean ORM dokumentacja wyraźnie mówi, że klasa modelu dla Play 2.5.x dziedziczyć ma po play.db.ebean.Model, natomiast dla JPA nie ma o tym wzmianki (przynajmniej nie znalazłem). W starszych wersjach Play (1.x), z tego co wyczytałem, była konieczność dziedziczenia po play.db.jpa.Model, jednak w 2.5.x nie widzę możliwości zrobienia takiego importu, pomimo dodania poniższych zależności, wyczyszczeniu projektu, przeładowania classpath i ponownym zbudowaniu.

libraryDependencies += "javax.persistence" % "persistence-api" % "1.0.2"
 
libraryDependencies ++= Seq(
  javaJpa,
  "org.hibernate" % "hibernate-entitymanager" % "5.2.8.Final"
)

Finalnie udało mi się znaleźć całkiem świeży przykład na GitHub, w którym nie stosuje się klasy nadrzędnej i tak też przyjmę, że powinno być. Najwyżej przyszły CRUD to zweryfikuje:]

Model jest, widoki (choć puste) również są, prosta konfiguracja zrobiona, kolej na kontrolery. Na tym etapie poprzestałem na przetestowaniu mechanizmu ładowania podstron. W moim szablonie w pliku main.scala.html przygotowałem menu, w którym znajdują się linki do kolejnych podstron.

<a href="@routes.HomeController.appartmentPlan()">Plan mieszkania</a>
<a href="@routes.HomeController.dashboard()">Panel informacyjny</a>
<a href="@routes.SensorController.list()">Czujniki</a>

Jak widać, parametr href wyraźnie wskazuje na metody w kontrolerach HomeController oraz SensorController.

public class HomeController extends Controller {
      public Result dashboard() {
              return ok(dashboard.render("Tablica informacyjna"));
      }
}

Wskazane metody póki co nie robią nic nadzwyczajnego, po prostu renderują widok. Wynikiem zwracanym jest Result. Metoda ok() pochodzi z klasy Results, która dostarcza standardowe wyniki generujące odpowiedź z odpowiednim kodem HTTP: badRequest(), created(), forbidden(), found(), internalServerError(), movedPermanently(), noContent(), notFound(), ok(), paymentRequired(), redirect(), seeOther(), status(), temporaryRedirect() oraz unauthorized().

Metoda dashboard() generuje widok z kodem 200 (ok) na podstawie szablonu dashboard.scala.html, który na tą chwilę wygląda jak poniżej. Ciąg znaków „Tablica informacyjna” jest przekazywany do widoku poprzez parametr message i wyświetlany w formie nagłówka podstrony.

@(message: String)
 
@main("Home Controller") {
    <h4>@message</h4>
    <br/>
}

Pozostaje jeszcze określenie routingu, czyli informacja jaka metoda ma być wykonywana gdy użytkownik wpisze wybrany adres w przeglądarce. Jak widać w pliku conf/routes określamy metodę HTTP (tutaj GET), adres żądania oraz metodę kontrolera obsługującego.

GET     /appartmentPlan                  controllers.HomeController.appartmentPlan()
GET     /dashboard                       controllers.HomeController.dashboard()
GET     /sensors/list                    controllers.SensorController.list()

W moim przypadku podstrony są dostępne pod poniższymi adresami.

http://localhost:9000/appartmentPlan
http://localhost:9000/dashboard
http://localhost:9000/sensors/list

To już chyba wszystko, co chciałem opisać w tym poście. Plany na przyszłość: czytać, czytać, czytać dokumentację. Jest trochę tego do przejrzenia, na co potrzeba czasu, jednak nie da się bez tego, bo błądzę po omacku. Jeśli chodzi o projekt, chcę połączyć się z bazą danych i przetestować działanie CRUD na jakimś prostym elemencie w Play oraz uruchomić zapis do MySQL po stronie RPI (Python). Pewnie kolejne posty będzą właśnie o tym.

Literatura obowiązkowa:

Zapach świeżej kawy: Play! Framework
Kickstart w Javie prawie jak w Ruby
Creating a new application
News Radar: Play Framework, Slick, Dependency Injection i podział na moduły

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *