rapido - aplikacja dla rowerzysty

Opis

Ostatnio napisałem sobie niewielką aplikację androidową, której zadaniem jest przechowywanie stanów licznika rowerowego i obliczanie oraz prezentacja podstawowych informacji statystycznych i serwisowych.

Ktoś mógłby powiedzieć po co, skoro jest już Endomondo (swoją drogą świetne ) . Może i tak, ale ja nie lubię reklam, zaproszeń, nie potrzebuję śladów GPS i oczekuję od aplikacji szacunku do mojej baterii.
A pozatym potrzebny mi jest ułamek dostępnych tam opcji, za to wymagam by były dostępne od razu, po jednym kliknięciu.

A pozatym wszystkim to chciałem się czegoś nowego nauczyć :)

I tak powstał mój mały projekt rapido

Źródła aplikacji (projekt w AndroidStudio)

Aplikacja *.apk

Czego aplikacja nie potrafi

  • Nie łączy się z Facebookiem, Twitterem i żadnym innym portalem społecznościowym
  • Nie korzysta z GPS
  • Nie potrzebuje dostępu do sieci

Co aplikacja daje w zamian

  • Pozwala na odkładanie kolejnych stanów licznika rowerowego
  • Prezentuje podstawowe statystyki, takie jak ostatnia przejechana trasa (jako różnica dwóch ostatnich stanów)
  • Prezentuje statystyki miesięczne (ilość km/miesiąc)
  • Prezentuje informacje serwisowe (ilość km przejechanych na tym samym łańcuchu, kasecie i od ostatniego smarowania)
  • Działa całkowicie offline, dane zapisuje w pliku tekstowym który można kopiować,edytować
  • Prawie nie zużywa baterii ;)
W chwili obecnej aplikacja składa się z trzech ekranów:
  • Ekran wprowadzania stanu licznika
  • Ekran na którym prezentowane są wybrane informacje zbiorcze
  • Ekran z którego można dowiedzieć się ile km temu odbyło się smarowanie lub wymiana łańcucha/kasety

Rejestracja stanu licznika

Podstawowy ekran aplikacji, pozwala na wprowadzenie stanu licznika.

Dostępne są dwie opcje wprowadzania:

  • Stan - wprowadzona liczba potraktowana jest jako stan licznika
  • Dystans - wprowadzona liczba traktowana jest jako odległość która ma zostać dodana do porzedniego stanu licznika
 Liczbę wprowadza się z klawiatury numerycznej i zatwierdza przyciskiem OK. Po zatwierdzeniu, wprowadzona liczba zostaje opatrzona stemplem czasowym i trafia do pliku rejestru.

Przed zapisem aplikacja waliduje czy wprowadzony ciąg znaków jest liczbą, a jeśli tak czy wprowadzany stan jest większy od poprzedniego.

Prezentacja statystyk i podsumować miesięcznych

Ekran ten podzielony jest na trzy sekcje:
  • Ostatni wpis - prezentacja ostatniego zarejestrowanego wpisu w rejestrze. Po odznaczeniu checkboxa "chroń przed usunięciem" możliwe jest jego usunięcie.
  • Ostatnia odległość - ostatnia trasa, czyli różnica pomiędzy dwoma ostatnimi wpisami w rejestrze.
  • Podsumowania miesięczne - przewijalna lista odległości przebytych w ostatnich miesiącach

Ekran prezentacji danych serwisowych

Na ekranie prezentowane są informacje istotne z punktu widzenia zużycia elementów napędowych.

Sekcja "Wpis serwisowy" służy do wprowadzania flag serwisowych. Za pomocą przycisków "<" ">" można poruszać się pomiędzy kolejnymi wpisami w rejestrze (w kolejności od najnowszego do najstarszego). Zaznaczenie checkboxa przy aktualnie wybranym rekordzie znaczy że przy danym stanie licznika nastąpiła dana czynność serwisowa.

Sekcja prezentująca dane dzieli się na trzy części
  • Kaseta
  • Łańcuch
  • Smarowanie
Pokazuje ona datę wykonania określonej czynności serwisowej, stan licznika przy którym się to odbyło i liczbę kilometrów stanowiącą różnicę pomiędzy ostatnim wpisem oznaczonym flagą i  stanem aktualnym licznika.



Biblioteka(AndroMDA+JSF+Spring+JPA) -część VI- model dziedziny

Ponieważ moja aplikacyjka nie będzie posiadała zbyt rozbudowanej funkcjonalności, sam model dziedziny również nie będzie zbyt finezyjny :).

Program ma umożliwić rejestrację Wypożyczeń(posiada daty od i do) i Rezerwacji(data rezerwacji) przez Czytelników zarejestrowanych w Bibliotece. Każdy Czytelnik jest Osobą posiadającą swoje imię i nazwisko, oraz numer biblioteczny. Z Czytelnikiem związana jest również lista jego Rezerwacji oraz Wypożyczeń. Rezerwacje i Wypożyczenia dotyczą Książek. Sama Książka posiada swój tytuł, oraz numer katalogowy.Każda Książka mogła zostać napisana przez wielu Autorów (jest to Osoba z imieniem i nazwiskiem, oraz unikalnym identyfikatorem autora).Każdy Autor z kolei mógł napisać wiele Książek.

Myślę że na początek to wystarczy. Pora wykonać model.

Uruchamiam ArgoUML z załadowanym profilem AndroMDA, zakładam pakiet "dziedzina" i przygotowuje projekt. Wygląda on mniej więcej tak:


Dokładna instrukcja, w oparciu o którą tworzyłem obiekty dziedziny zamieszczona jest na stronach dokumentacji cartridge'a EJB3 .

Według mnie na kilka, absolutnie kluczowych zasad należy zwrócić szczególną uwagę.

Przede wszystkim, do naszego projektu powinien zostać załadowany profil AndroMDA dla ArgoUML (o sposobie jego instalowania i osadzania w projekcie pisałem we
wcześniejszym poście
).

Po drugie, wszystkie encje muszą być oznaczone stereotypem "Entity".

Po trzecie, typy dla atrybutów encji muszą pochodzić z załadowanego profilu.

W przypadku ArgoUML, typ atrybutu pochodzący z profilu wygląda tak:


Skoro mój model dziedziny jest już gotowy pora wygenerować kod. W tym celu eksportuje model do formatu xmi (File-Export XMI...). Wygenerowany plik zapisuję jako "biblioteka.xmi" w ścieżce "mda/src/main/uml/biblioteka.xmi".


Teraz tylko uruchomienie pluginu



i następuje przetwarzanie modelu:

INFO  [AndroMDA]   +  registering component 'translation-library'
INFO  [AndroMDA] - core initialization complete: 13.07[s] -
INFO  [AndroMDA] loading model --> 'file:/dane/projekty/sandbox/biblioteka/mda/src/main/uml/biblioteka.xmi'
INFO  [AndroMDA] referenced model --> 'file:/dane/projekty/sandbox/biblioteka/mda/src/main/uml/andromda-profile-32-noextensions.xmi'
INFO  [AndroMDA] - loading complete: 6.005[s] -
INFO  [AndroMDA] - validating model -

Klasy wynikowe, zgodnie z przygotowaną przeze mnie konfiguracją pluginu AndroMDA trafiają do modułu "model", do pakietu "dziedzina"


Kod dla najbardziej złożonej klasy Czytelnik wygląda następująco:

// license-header java merge-point
//
// Attention: Generated code! Do not modify by hand!
// Generated by: EntityEmbeddable.vsl in andromda-ejb3-cartridge.
//
package dziedzina;

import java.io.Serializable;
import java.util.Set;
import java.util.TreeSet;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

/**
 * Autogenerated POJO EJB class for Czytelnik containing the
 * bulk of the entity implementation.
 *
 * This is autogenerated by AndroMDA using the EJB3
 * cartridge.
 *
 * DO NOT MODIFY this class.
 *
 * 
 */
@Entity
@DiscriminatorValue("C")
@NamedQuery(name = "Czytelnik.findAll", query = "select czytelnik from Czytelnik AS czytelnik")
public class Czytelnik
    extends Osoba
    implements Serializable
{
    private static final long serialVersionUID = 7232739200991516470L;

    // ----------- Attribute Definitions ------------

    private String numerBiblioteczny;

    // --------- Relationship Definitions -----------

    private Set<Wypozyczenie> listaWypozyczen = new TreeSet<Wypozyczenie>();
    private Set<Rezerwacja> listaRezerwacji = new TreeSet<Rezerwacja>();
    // ---- Manageable Display Attributes (Transient) -----


    // --------------- Constructors -----------------

    /**
     * Default empty constructor
     */
    public Czytelnik() {}

    /**
     * Implementation for the constructor with all POJO attributes except auto incremented identifiers.
     * This method sets all POJO fields defined in this class to the values provided by
     * the parameters.
     *
     * @param imie Value for the imie property
     * @param nazwisko Value for the nazwisko property
     * @param numerBiblioteczny Value for the numerBiblioteczny property
     */
    public Czytelnik(String imie, String nazwisko, String numerBiblioteczny)
    {
        setImie(imie);
        setNazwisko(nazwisko);
        setNumerBiblioteczny(numerBiblioteczny);
    }

    /**
     * Constructor with all POJO attribute values and CMR relations.
     *
     * @param imie Value for the imie property
     * @param nazwisko Value for the nazwisko property
     * @param numerBiblioteczny Value for the numerBiblioteczny property
     * @param listaWypozyczen Value for the listaWypozyczen relation
     * @param listaRezerwacji Value for the listaRezerwacji relation
     */
    public Czytelnik(String imie, String nazwisko, String numerBiblioteczny, Set<Wypozyczenie> listaWypozyczen, Set<Rezerwacja> listaRezerwacji)
    {
        setImie(imie);
        setNazwisko(nazwisko);
        setNumerBiblioteczny(numerBiblioteczny);

        setListaWypozyczen(listaWypozyczen);
        setListaRezerwacji(listaRezerwacji);
    }

    // -------- Attribute Accessors ----------

    /**
     * Get the numerBiblioteczny property.
     * 
     * @return String The value of numerBiblioteczny
     */
    @Column(name = "NUMER_BIBLIOTECZNY", insertable = true, updatable = true)
    public String getNumerBiblioteczny()
    {
        return numerBiblioteczny;
    }

    /**
     * Set the numerBiblioteczny property.
     * @param value the new value
     */
    public void setNumerBiblioteczny(String value)
    {
        this.numerBiblioteczny = value;
    }

    // ------------- Relations ------------------

    /**
     * Get the listaWypozyczen Collection
     *
     * @return Set<Wypozyczenie>
     */
    @OneToMany(mappedBy = "czytelnik")
    public Set<Wypozyczenie> getListaWypozyczen()
    {
        return this.listaWypozyczen;
    }

    /**
     * Set the listaWypozyczen
     *
     * @param listaWypozyczen
     */
    public void setListaWypozyczen (Set<Wypozyczenie> listaWypozyczen)
    {
        this.listaWypozyczen = listaWypozyczen;
    }

    /**
     * Get the listaRezerwacji Collection
     *
     * @return Set<Rezerwacja>
     */
    @OneToMany(mappedBy = "czytelnik")
    public Set<Rezerwacja> getListaRezerwacji()
    {
        return this.listaRezerwacji;
    }

    /**
     * Set the listaRezerwacji
     *
     * @param listaRezerwacji
     */
    public void setListaRezerwacji (Set<Rezerwacja> listaRezerwacji)
    {
        this.listaRezerwacji = listaRezerwacji;
    }

    // -------- Common Methods -----------

    /**
     * Indicates if the argument is of the same type and all values are equal.
     *
     * @param object The target object to compare with
     * @return boolean True if both objects a 'equal'
     */
    public boolean equals(Object object)
    {
        if (this == object)
        {
            return true;
        }
        if (!(object instanceof Czytelnik))
        {
            return false;
        }
        final Czytelnik that = (Czytelnik)object;
        if (this.getId() == null || that.getId() == null || !this.getId().equals(that.getId()))
        {
            return false;
        }
        return true;
    }

    /**
     * Returns a hash code value for the object
     *
     * @return int The hash code value
     */
    public int hashCode()
    {
        int hashCode = super.hashCode();
        hashCode = 29 * hashCode + (getId() == null ? 0 : getId().hashCode());

        return hashCode;
    }

    /**
     * Returns a String representation of the object
     *
     * @return String Textual representation of the object displaying name/value pairs for all attributes
     */
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("Czytelnik(=");
        sb.append(super.toString());
        sb.append("numerBiblioteczny: ");
        sb.append(getNumerBiblioteczny());
        sb.append(")");
        return sb.toString();
    }

}

Żeby dowiedzieć się czy wygenerowany kod jest naprawdę w porządku, przygotuję kilka testów jednostkowych. Tyle że opiszę je już następnym razem....

Biblioteka(AndroMDA+JSF+Spring+JPA) -część V- mapowanie relacji

No i się doczekałem :) konfiguracja skończona. Wreszcie mam środowisko na którym mogę naprawdę się pobawić. Tak jak już pisałem zacznę od modelu dziedziny. Zanim jednak zamodeluję sobie kawałek aplikacji postanowiłem przyjrzeć się jak wygląda kod encji wygenerowany przez framework.Oto krótki przegląd mojego eksperymentu.

Relacje @One-To-One


Asocjacja dwukierunkowa
Na początek spróbuję sprawdzić relację One-To-One zamodelowaną za pomocą relacji asocjacji dwukierunkowej.


Kod po stronie klasy Przedsiebiorca.java
@OneToOne(mappedBy = "przedsiebiorca")
public Adres getAdresDzialalnosciGospodarczej()
{
return this.adresDzialalnosciGospodarczej;
}

Kod po stronie klasy Adres.java
@OneToOne(mappedBy = "adresDzialalnosciGospodarczej")
public Przedsiebiorca getPrzedsiebiorca()
{
return this.przedsiebiorca;
}

No i niestety... od razu problem, bo kod nie jest poprawny. Po obydwu stronach relacji wygenerowana została adnotacja z atrybutem "mappedBy" - framework nie poradził sobie z określeniem właściciela relacji. A więc tak się nie da. Skoro więc nie tak.. to pozostaje pytanie "jak"?
Wskazówkę niesie dokumentacja:

 "To model the owning side of a One-To-One or Many-To-Many bidirectional relationship, you indicate that end (the owning end) of the relationship as an aggregate or composite end. "

A więc wygląda na to, że dwukierunkowa  relacja pomiędzy encjami musi być modelowana przy pomocy agregacji lub kompozycji...Szkoda, bo przecież nie każdy związek pomiędzy obiektami to od razu agregacja lub kompozycja...

Agregacja dwukierunkowa
No to skoro wiem, co mówi teoria, pora spróbować praktyki, jeśli ma być agregacja, to niech będzie agregacja:

Kod po stronie klasy Przedsiebiorca.java
@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "ADRES_DZIALALNOSCI_GOSPODAR_FK")
public Adres getAdresDzialalnosciGospodarczej()
{
return this.adresDzialalnosciGospodarczej;
}

Kod po stronie klasy Adres.java
@OneToOne(mappedBy = "adresDzialalnosciGospodarczej")
public Przedsiebiorca getPrzedsiebiorca()
{
return this.przedsiebiorca;
}

A więc tereaz generata jest w porządku. Właściciel relacji został prawidłowo wyodrębniony. Po jego stronie wygenerowany został klucz obcy - więc wygląda że wszystko jest ok.


Asocjacja jednokierunkowa
Relację skierowaną wypróbuję sobie na podstawie asocjacji.



Kod po stronie klasy Przedsiebiorca.java
@OneToOne(optional = false)
@JoinColumn(name = "ADRES_DZIALALNOSCI_GOSPODAR_FK")
public Adres getAdresDzialalnosciGospodarczej()
{
return this.adresDzialalnosciGospodarczej;
}

Kod po stronie klasy Adres.java
BRAK

A więc w przypadku relacji skierowanych, kod wygenerowany na podstawie asocjacji jest już całkowicie poprawny.

Relacje @One-To-Many



Asocjacja dwukierunkowa



Kod po stronie klasy Przedsiebiorca.java
@OneToMany(mappedBy = "przedsiebiorca")
public Set getAdresy()
{
return this.adresy;
}

Kod po stronie klasy Adres.java
@ManyToOne(optional = false)
@JoinColumn(name = "PRZEDSIEBIORCA_FK")
public Przedsiebiorca getPrzedsiebiorca()
{
return this.przedsiebiorca;
}

Powyższy kod jest poprawny, więc wygląda na to, że relacja @OneToMany może być modelowana przy pomocy relacji asocjacji dwukierunkowej.

Ale zaraz... coś tu jest nie tak... właścicielem relacji w UMLu jest klasa Przedsiebiorca.java, a klucz główny został wygenerowany po stronie Adres.java, dlaczego? A więc krótka wycieczka do specyfikacji "ejb-3_0-fr-spec-persistence" i wszystko jasne...
"The many side of one-to-many / many-to-one bidirectional relationships must be the owning
side, hence the mappedBy element cannot be specified on the ManyToOne annotation." 


Właściciel relacji po stronie JPA jest determinowany krotnością elementów, a nie kierunkiem relacji UML-owej. A więc wszystko jasne...

Agregacja dwukierunkowa




Kod po stronie klasy Przedsiebiorca.java
@OneToMany(mappedBy = "przedsiebiorca")
public Set getAdresy()
{
return this.adresy;
}

Kod po stronie klasy Adres.java
@ManyToOne(optional = false)
@JoinColumn(name = "PRZEDSIEBIORCA_FK")
public Przedsiebiorca getPrzedsiebiorca()
{
return this.przedsiebiorca;
}

Tutaj również wygenerowany kod jest poprawny.

Asocjacja jednokierunkowa



Kod po stronie klasy Przedsiebiorca.java
@OneToMany()
@JoinTable
(
name = "PRZEDSIEBIORCA2ADRESY",
joinColumns = {@JoinColumn(name = "PRZEDSIEBIORCA_ID_FK", referencedColumnName = "ID")},
inverseJoinColumns = {@JoinColumn(name = "ADRESY_ID_FK", referencedColumnName = "ID")}
)
public Set getAdresy()
{
return this.adresy;
}

Kod po stronie klasy Adres.java
Brak

Relacje @Many-To-Many

Znany cytat z dokumentacji AndroMDA mówi, że relacja @ManyToMany powinna być modelowana agregacją dwukierunkową.
 "To model the owning side of a One-To-One or Many-To-Many bidirectional relationship, you indicate that end (the owning end) of the relationship as an aggregate or composite end. "



Agregacja dwukierunkowa




Kod po stronie klasy Firma.java
@ManyToMany()
@JoinTable
(
name = "FIRMY2LOKALIZACJE",
joinColumns = {@JoinColumn(name = "FIRMY_ID_FK", referencedColumnName = "ID")},
inverseJoinColumns = {@JoinColumn(name = "LOKALIZACJE_ID_FK", referencedColumnName = "ID")}
)
public Set getLokalizacje()
{
return this.lokalizacje;
}

Kod po stronie klasy Lokalizacja.java
@ManyToMany(mappedBy = "lokalizacje")
public Set getFirmy()
{
return this.firmy;
}

A więc z tą relacją plugin również nie miał problemów. :)

Podsumowanie

Ten krótki przegląd możliwości pluginu w zakresie generowania encji przekonał mnie, że jest on frameworkiem wartym uwagi. Wygenerowany kod źródłowy jest dobrej jakości, a zachowanie pluginu przewidywalne i co rzadko spotykane - dobrze opisane w dokumentacji.
Następnym razem skupię się już na praktycznym zastosowaniu AndroMDA. Opiszę model dziedziny mojej małej aplikacji i sprawdzę czy wygenerowany kod działa na środowisku docelowym (a więc Spring + Tomcat)....

Tymczasem spaaaaać... :)...

Biblioteka(AndroMDA+JSF+Spring+JPA) -część IV- konfiguracja frameworka

Ilość parametrów obsługiwanych przez AndroMDA jest imponująca. Framework jest elastyczny i daje się łatwo dopasowywać do specyfiki projektu. Konfiguracja odbywa się przez parametryzację dwóch plików xml. Nie zamierzam wypisywać wszystkich możliwych do wprowadzenia opcji (tym bardziej, że są one dostępne w oficjalnej dokumentacji). Chciałbym jednak pokrótce wymienić najważniejsze fragmenty konfiguracji i opisać ustawione przeze mnie parametry.

pom.xml (/mda/pom.xml)

Zawartość pliku pom.xml modułu mda opisywałem już jakiś czas temu, dlatego nie będę się na nim skupiał.  Ponieważ na początku mojej przygody z AndroMDA chciałbym ograniczyć się jedynie do wygenerowania persystentnego modelu dziedziny , potrzebny mibędzie tylko jeden cartridge, który posłuży do wygenerowania kodu encji (JPA).

Z tego powodu z sekcji zależności usunąłem prawie całą zawartość, a jedynym wpisem pozostał:

<dependency>
<groupId>org.andromda.cartridges</groupId>
<artifactId>andromda-ejb3-cartridge</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
</dependency>

Dodatkowo, założyłem sobie, że kody wygenerowane przez AndroMDA trafiały będą do modułu "model", dlatego w sekcji właściwości zdefiniowałem sobie stałą wskazującą na umiejscowienie kodów źródłowych tego modułu:

<model.dir>${project.basedir}/../model/src/main/java/</model.dir>

będę się do niej odwoływał, przy konfiguracji cartridge ejb3.

Konfiguracja sposobu przetwarzania (/mda/src/main/config/andromda.xml)

Sercem konfiguracji frameworku AndroMDA jest plik /mda/src/main/config/andromda.xml . Posiada on dosyć czytelną strukturę, która została dobrze opisana w dokumentacji projektu.

1. Sekcja properties
W sekcji tej definiowane są parametry definiujące sposób przetwarzania modelu. Według mnie na szczególną uwagę zasługują dwie flagi:
  • modelValidation - określa czy model jest awalidowany pod kątem zgodności z profilem 
  • outputEncoding - kodowanie plików wynikowych
<properties>
<property name="modelValidation">true</property>
<property name="outputEncoding">true</property>
</properties>

2. Sekcja repositories
W sekcji tej definiowane są źródła, z których zaczytywany jest model, oraz wskazywane są pliki umożliwiające prawidłowe przetwarzanie jego przetwarzanie. Najważniejsze parametry, które ustawić  można w tej sekcji definiowane są w elemencie "model":
  • uri - pozwala na wskazanie ścieżki do pliku modelu
  • moduleSearchLocations - umożliwia wskazanie miejsca, z którego zaczytywane mają być pliki konieczne do przetwarzania modelu (np. pliki profilu)
W moim przypadku sekcja ta wygląda następująco:

<repository name="netBeansMDR">
<models>
<model>
<uri>file:${project.basedir}/src/main/uml/biblioteka.xmi</uri>
<moduleSearchLocations>
<location patterns="**/*.xmi">${project.basedir}/src/main/uml/</location>
</moduleSearchLocations>
</model>
</models>
</repository>
</repositories>


3. Sekcja mappingsSearchLocations
Sekcja ta pozwala na wskazanie lokalizacji, w której znajdują się pliki zawierające mapowania typów umlowych na typy Java.
<mappingsSearchLocations>
<location patterns="*.xml">${conf.dir}/mappings</location>
</mappingsSearchLocations>

4. Sekcja namespaces
Jest to kluczowa sekcja, w której zdefiniowane są aktywne cartridge - czyli biblioteki dokonujące faktycznych przekształceń kodu. Dla każdego z cartridgy możliwe jest podanie całego zatrzęsienia parametrów, które w znaczący sposób dopasowują generatę do indywidualnej specyfiki projektu. W tym miejscu należy zwrócić uwagę na fakt, że każdy użyty cartridge musi zostać wymieniony w /mda/pom.xml w sekcji zależności.
Dobór cartridgy ma decydujący wpływ na formę otrzymanych plików wyjściowych. Np. użycie cartridge "spring" spowoduje wygenerowanie beanów springowych, natomiast cartridge "ejb3" pozwala na wygenerowanie kodów zgodnych z ejb3.

Szczególne znaczenie ma namespace o nazwie "default". Parametry w nim zdefiniowane będą wpływały na każdy inny cartridge.

Na stronie projektu, w sekcjach poświęconych poszczególnym cartridgom możemy odnaleźć dokładną listę parametrów konfiguracyjnych obsługiwanych przez dany cartridge (np dla c. ejb3 strona ta wygląda następująco).

<namespaces>
<namespace name="default">
<properties>
<property name="enableTemplating">true</property>
<property name="enableAnnotations">true</property>
<property name="typeSafeEnumsEnabled">true</property>
<property name="languageMappingsUri">JavaMappings</property>
<property name="wrapperMappingsUri">JavaWrapper</property>
<property name="sqlMappingsUri">${sql.mappings}</property>
<property name="jdbcMappingsUri">JDBC</property>
<property name="maxSqlNameLength">30</property>
<property name="foreignKeySuffix">_FK</property>
<property name="ejbJndiNamePrefix">${application.id}-${project.version}</property>
<property name="enumerationLiteralNameMask">upperunderscore</property>
<property name="persistenceContainerName">jboss</property>
<property name="pluralizeAssociationEndNames">false</property>
<property name="pluralizeAttributeNames">false</property>
<property name="pluralizeParameterNames">false</property>
</properties>
</namespace>

<namespace name="ejb3">
<properties>
<property name="entity-beans">${model.dir}</property>
</properties>
</namespace>
</namespaces>

W powyższej konfiguracji chciałbym zwrócić uwagę na wpis dotyczący cartridgea ejb3
<property name="entity-beans">${model.dir}</property>
Definiuje on miejsce do którego trafiały będą wytworzone artefakty. Stała ${model.dir} zdefiniowana została w /mda/pom.xml .

Biblioteka(AndroMDA+JSF+Spring+JPA) -część III - baza danych

Przed rozpoczęciem pracy nad tworzeniem mojej małej aplikacji muszę zainstalować sobie bazę, w której będe przechowywał dane, oraz wygodne narzędzie, które pozwoli mi na bieżąco śledzić
zmiany w tabelach.

1. Instalacja HSQLDB
Jako bazki danych użyję HSQLDB w wersji 1.8.0. Uruchamiam ją w trybie serwera standalone.

mw@mw:/dane/java/hsqldb-1.8.0/hsqldb/bin> java -cp ../lib/hsqldb.jar org.hsqldb.Server

Po wyświetleniu komunikatów startowych baza wstaje i oczekuje na połączenia.

[Server@156ee8e]: [Thread[main,5,main]]: checkRunning(false) entered
[Server@156ee8e]: [Thread[main,5,main]]: checkRunning(false) exited
[Server@156ee8e]: Startup sequence initiated from main() method
[Server@156ee8e]: Loaded properties from [C:\sandbox\programy\hsqldb_1_8_0_10\hsqldb\bin\server.properties]
[Server@156ee8e]: Initiating startup sequence...
[Server@156ee8e]: Server socket opened successfully in 9 ms.
[Server@156ee8e]: Database [index=0, id=0, db=file:mydb, alias=xdb] opened sucessfully in 273 ms.
[Server@156ee8e]: Startup sequence completed in 291 ms.
[Server@156ee8e]: 2011-09-20 16:03:56.868 HSQLDB server 1.8.0 is online
[Server@156ee8e]: To close normally, connect and execute SHUTDOWN SQL
[Server@156ee8e]: From command line, use [Ctrl]+[C] to abort abruptly

2. Instalacja klienta SQL
Do podglądania zawartości tabel użyję prostego klienta SQL SquirrelSQL (http://www.squirrelsql.org/)


3. Instalacja driverów JDBC i definicja połączenia z serwerem
Żeby podłączyć się do serwera DB, konieczne jest wgranie driverów JDBC. W przypadku HSQLDB należy odnaleźć plik hsqldb.jar (HSQLDB_HOME/lib) i przegrać go do katalogu SQUIRREL_HOME/lib. Po restarcie klienta, drivery automatycznie stają się dostępne i można zdefiniować połączenie do serwera.



Przy definicji połączenia kluczowe jest podanie właściwej wartości Connection String, w moim przypadku ma on postać:
"jdbc:hsqldb:hsql://localhost/xdb"


Teraz tylko należy połączyć się z serwerem i możemy oglądać sobie zawartość naszych tabel.

Definicja springowego DataSource

Teraz muszę zadbać o to by mój projekt korzystał z przygotowanego przeze mnie połączenia do bazy. Muszę odnaleźć definicję springowego DataSource.
Nie jest to trudne, po chwili przeszukiwania trafiam na WEB-INF/config/data-access-config.xml.
Tam odnalazłem definicję źródła danych i ustawiłem właściwy url.
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver" />
<property name="url" value="jdbc:hsqldb:hsql://localhost/xdb"/>
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
Ta zmiana spowoduje, że po inicjalizacji kontekstu springowego, źródłem danych mojej aplikacji będzie przygotowana wcześniej baza danych.

Biblioteka(AndroMDA+JSF+Spring+JPA) -część II - struktura projektu

Wstęp


Chciałbym, aby szkieletem mojej małej aplikacji był Spring Web Flow,warstwę prezentacji zrealizowana była w oparciu o facelety, a warstwa persystentną - w oparciu o JPA. Teraz tylko pozostaje mi znalezienie bazowego projektu mavenowego, w którym zdefiniowane byłyby wymienione przeze mnie zależności(albo chociaż ich część).

Na liście archetypów mavenowych nie znalazłęm tego, czego potrzebuję.

Zajrzałem zatem do przykładowych projektów dostarczanych wraz z dystrybucją Spring Web Flow. I tam natrafiłem na projekt "booking-facets", który według dokumentacji jest zrealizowany w:



* booking-faces: Spring MVC + Web Flow + JavaServerFaces


a więc wygląda że jest dokładnie tym, czego szukam:).

Struktura projektu


Płaska i jednomodułowa struktura projektu "booking-facets" nie bardzo mi się podoba. Na szczęście, dzięki zastosowaniu Mavena, nie jest to problem.Na potrzeby mojej małej aplikacji wydzieliłem sobie kilka modułów powiązanych wzajemnymi relacjami:


mda
model
uslugi
web

  • mda - moduł AndroMDA, którego zadaniem będzie realizacja przekształcenia (PIM->PSM) i generowanie kodu źródłowego modelu na podstawie dostarczonego xmi.
  • model - moduł do którego trafiały będą pliki źródłowe wygenerowane przez AndroMDA. Każda generata modelu będzie czyściła i nadpisywała poprzednią jego zawartość.
  • usługi - do modułu trafiała będzie wykonywana przez programistę implementacja usług działających w oparciu o wygenerowany model. Tu niestety pojawia się problem, bo zawartość modułu nie może być czyszczona po każdej generacie. Różnice wynikające ze zmian modelu muszą być świadomie i "ręcznie" wprowadzone do modelu.
  • web - aplikacja webowa, realizująca GUI, oraz prawidłowy przepływ sterowania
Strukturę projektu, zaimportowanego do Idea pokazuje poniższy zrzut.




Zależności


Jeśli chodzi o zależności to przykładowy projekt springowy potraktowałem prawie jako gotowca... z jedną małą różnicą. Mianowicie główny pom projektu musiał zostać rozbudowany o bibliteki, repozytoria, oraz właściwości wykorzystywane przez plugin AndroMDA (bo przecież do projektu włączyłem go na skutek własnej inicjatywy :)).

Teraz kilka słów o tym jak zrealizowałem to właśnie to włączenie... Posłużyłem się aplikacją wygenerowaną przy pomocy pluginu org.andromda.maven.plugins:andromdapp-maven-plugin:3.4-SNAPSHOT , którą opisałem w jednym z moich poprzednich postów. Do mojego projektu przeniosłem cały wygenerowany tam moduł "mda", a pomy uzupełniłem na wzór stworzonych tam zależności.

Wybrane fragmenty głównego pom.xml mojego projektu, które dotyczą obsługi frameworka AndroMDA wyglądają następująco:

Sekcja właściwości
<properties>
<andromda.version>3.4-SNAPSHOT</andromda.version>
<sql.mappings>
HypersonicSql
</sql.mappings>
<hibernate.dialect>
</hibernate.dialect>
<application.id>mdaprojekt</application.id>
<application.package>mw</application.package>
<application.name>Projekt eksperymentalny</application.name>
<application.version>1.0-SNAPSHOT</application.version>
<dataSource.name>jdbc/${application.id}</dataSource.name>
<dataSource>java:comp/env/${dataSource.name}</dataSource>
</properties>

Sekcja zależności
<dependencies>

<!--AndroMDA-->
<dependency>
<groupId>org.andromda</groupId>
<artifactId>andromda-core</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.andromda.translationlibraries</groupId>
<artifactId>andromda-ocl-translation-core</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.andromda.translationlibraries</groupId>
<artifactId>andromda-ocl-validation-library</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.andromda.cartridges</groupId>
<artifactId>andromda-jsf-cartridge</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.andromda.cartridges</groupId>
<artifactId>andromda-jsf-cartridge-components</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.andromda</groupId>
<artifactId>andromda-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.andromda.metafacades</groupId>
<artifactId>andromda-metafacades-uml</artifactId>
</exclusion>
<exclusion>
<groupId>org.andromda.translationlibraries</groupId>
<artifactId>andromda-ocl-validation-library</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.andromda</groupId>
<artifactId>andromda-utils</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.andromda</groupId>
<artifactId>andromda-core</artifactId>
</exclusion>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
<exclusion>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
</exclusion>
<exclusion>
<groupId>xalan</groupId>
<artifactId>xalan</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.andromda.translationlibraries</groupId>
<artifactId>andromda-ocl-query-library</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- This dependency provides the ability to implement your
business logic in an interpreted manner during development on
your local machine -->
<dependency>
<groupId>org.andromda</groupId>
<artifactId>andromda-script-wrappers</artifactId>
<version>${andromda.version}</version>
<scope>runtime</scope>
</dependency>

</dependencies>

Sekcja definicji repozytoriów
<repositories>
<!--AndroMDA -->
<repository>
<id>sonatype</id>
<name>Sonatype AndroMDA Repository</name>
<url>http://oss.sonatype.org/content/groups/public</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>sonatype</id>
<name>Sonatype AndroMDA Repository</name>
<url>http://oss.sonatype.org/content/groups/public</url>
</pluginRepository>
</pluginRepositories>

Definicja pluginu
<plugins>
<!--AndroMDA-->
<plugin>
<groupId>org.andromda.maven.plugins</groupId>
<artifactId>andromda-maven-plugin</artifactId>
<version>${andromda.version}</version>
</plugin>

</plugins>

Jeszcze tylko zwyczajowe

mvn clean install

i uspokajający wynik
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Biblioteka : Spring MVC + Web Flow + JSF/PrimeFaces SUCCESS [1.958s]
[INFO] Biblioteka - mda module ........................... SUCCESS [37.468s]
[INFO] Biblioteka - model ................................ SUCCESS [3.717s]
[INFO] Biblioteka - uslugi ............................... SUCCESS [4.081s]
[INFO] Biblioteka - web module ........................... SUCCESS [24.342s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------


dają nadzieję że o niczym nie zapomniałem :)

A więc opisałem przygotowanie ArgoUML, oraz strukturę mojego projektu. W następnym kroku kilka słów o konfiguacji AndroMDA i można zacząć zabawę :)...

Biblioteka(AndroMDA+JSF+Spring+JPA) -część I- profil dla ArgoUML

Wstęp


Szybki przegląd dokumentacji udostępnionej na stronach projektu AndroMDA zachęcił mnie do praktycznego poeksperymentowania z tym frameworkiem. Założyłem sobie, że zaprojektuje, a potem dokonam implementacji małej, przykładowej aplikacji, która będzie realizowała podstawowe funkcjonalności prostej biblioteki.
Projekt przygotuję w ArgoUML, a AndroMDA posłuży mi jako narzędzie generowania kodu dla Spring Web Flow, którego będę chciał użyć do faktycznej implementacji.

Profil UML

Podstawowym zadaniem, które musi realizować framework bazujący na koncepcji MDA jest wykonywanie automatycznych przekształceń modelu. To z pozoru proste zadanie jest skutecznie komplikowane przez samą naturę języka UML, który dopuszcza dosyć dużą dowolność w posługiwaniu się jego elementami.

W przypadku tworzenia modelu PIM musimy zadbać, by framework został poinformowany o naszych intencjach dotyczących natury definiowanego obiektu (trochę teorii o Model Instance Mapping). Na poziomie realizacji przekształcenia do PSM podjęta musi zostać właściwa decyzja o charakterze generowanego artefaktu (przykładowo inny kod musi być wygenerowany dla obiektu dziedziny - encji, a inny dla bezstanowej klasy usługowej).

Aby rozwiązać ten problem, konieczne jest posługiwaniem się pewnym usystematyzowanym zbiorem reguł i ograniczeń, który będzie zrozumiały zarówno dla projektanta modelu, jak i frameworku dokonującego przekształceń. Stanowi on rodzaj klucza, w oparciu o który wykonana zostanie transformacja.

W przypadku AndroMDA, reguły te zostały opisane przy pomocy Profilu UML. Jest to plik "xmi", w którym zdefiniowany został zestaw typów,stereotypów i tagów, które wykorzystane mogą zostać do tworzenia modelu. Plik ten jest importowany do narzędzia uml, a następnie na etapie konfigurowania frameworku podawany jako podstawa realizacji przekształcenia. Tu należy zwrócić uwagę na fakt, że takie podejście wymusza dużą dyscyplinę pracy przy tworzeniu modelu. Skorzystanie z jakichkolwiek typów, stereotypów, czy elementów spoza dopuszczanych przez profil spowoduje, że model nie przejdzie etapu walidacji i transformacja zakończy się błędem.

Instalacja profilu AndroMDA dla ArgoUML



Na początek należy odnaleźć profil UML przeznaczony dla ArgoUML, który pozwoli nam na właściwe przygotowanie naszego modelu PIM. Nie jest to trudne, krótkie poszukiwanie w Google i trafiam we właściwe miejsce. Ze strony profilu, pobieram zamieszczony tam plik (andromda-profile-32-noextensions.xmi) i zapisuję go na dysk.
Teraz kilka kroków instalacji w ArgoUML i wreszcie można zabrać się do pracy.

1. Rejestracja profilu domyślnego (Edit->Settings->Profiles)
Na początku możemy zarejestrować nasz profil. W tym celu, w ArgoUML uruchamiamy opcję Edit->Settings->Profiles. Otwiera się okno pokazane na zrzucie:



Należy użyć przycisku "Load profile from file...", a następnie wskazać nasz profil. Po zaczytaniu, w polu "Available Profiles" pojawi się nowy wpis. Potem już tylko należy go wskazać i kliknąć w przycisk ">>". Zostanie przeniesienie go do pola "Default Profiles:". U mnie wyglądało to następująco:

Potem "Ok" i zamykamy okienko. Profil jest zarejestrowany.

3.File->Project Properties->Profiles

Teraz należy zadbać o to, by zarejestrowany profil został skojarzony z naszym projektem. W tym celu zakładamy nowy projekt ( u mnie biblioteka.zargo) i uruchamiamy opcje File->Project Properties->Profiles . W okienku "Active Profiles" ma zostać wpis wskazujący na nasz profil. Ma to wyglądać mniej więcej tak:



P.S. Dopiero teraz zauważyłem że okno posiada własny przycisk "Load profile from file...", więc przypuszczalnie kroki opisane w Rejestracji Profilu są zbędne i punkt 1 mógłby spokojnie wylecieć z opisu, ale niech już zostanie :) - może ktoś kiedyś będzie chciał zarejestrować profil domyślny to będą jak znalazł :).

Opisałem w jaki sposób przygotowałem sobie ArgoUML do pracy z AndroMDA. Następnym razem słów kilka o strukturze i organizacji mojego małego projektu.

AndroMDA - pierwsze spotkanie

W poprzednim moim wpisie wspomniałem że będę próbował przetestować eclipsowe narzędzia wspierające tworzenie aplikacji w oparciu o MDA. Naprawdę próbowałem, jednak przyznam szczerze, że środowiska do projektowania, oparte na Eclipse zwyczajnie mi nie leżą, a pozatym sprawiają wrażenie surowych i mocno niedopracowanych.

Po chwilowym zniechęceniu pojawił się promyk nadziei , ponieważ trafiłem na darmowe narzędzie, które bardzo pozytywnie mnie zaskoczyło - mianowicie ArgoUML .

W pierwszej chwili pomyślałem, że dałoby się używać ArgoUML jako narzędzia do projektowania, a frameworku EMF do wykonywania transformat MDA. W teorii pomysł wydawał się ciekawy, ale na drodze do jego realizacji wyrosła niespodziewana przeszkoda - niezgodność formatów.

W ArgoUML, naturalny format wyjściowy to XMI, natomiast we frameworku EMF na wejściu potrzebny jest format ECORE (swoją drogą temat "przenośności" formatu xmi poruszę innym razem) . Co prawda istnieje projekt który potrafi dokonać konwersji jednego w drugi... jednak wydaje mi się to dosyć dużą komplikacją, która sprawia że cała gra niewarta jest świeczki.

Kiedy znowu wydawało się, że sprawa jest beznadziejna, trafiłem na link do projektu AndroMDA.

Jest to framework, który realizuje koncepcje przetwarzania modelu aplikacji opisywaną przez MDA. Celem projektu AndroMDA jest dostarczenie mechanizmów pozwalających na realizację konwersji pomiędzy modelami PIM a PSM, dzięki którym projektant aplikacji zostaje odciążony od konieczności wykonywania żmudnych i w dużej mierze automatycznych przekształceń.

Ze względu na to, że działa w oparciu o xmi i wspiera modele wygenerowane przez ArgoUML, w tunelu beznadziei pojawiło się malutkie światełko :)...

Instalacja pluginów AndroMDA



Po chwilowych problemach z zależnościami mavena i właściwym ustawieniu repozytoriów, oraz podparciu się informacjami opublikowanymi na

http://debugtrue.blogspot.com/2007/06/installing-andromda-plugin-for-maven-2.html

udało mi się przygotować pom.xml, który prawidłowo zainstalował w moim lokalnym repozytorium pluginy mavenowe.



<project>
<modelversion>4.0.0</modelversion>
<groupid>samples.test</groupid>
<artifactid>test</artifactid>
<version>1.0</version>
<packaging>jar</packaging>
<name>test</name>

<build>
<defaultgoal>compile</defaultgoal>
<plugins>
<plugin>
<groupid>org.andromda.maven.plugins</groupid>
<artifactid>andromda-maven-plugin</artifactid>
<version>3.4-SNAPSHOT</version>
</plugin>
<plugin>
<groupid>org.andromda.maven.plugins</groupid>
<artifactid>andromdapp-maven-plugin</artifactid>
<version>3.4-SNAPSHOT</version>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>andromda</id>
<name>AndroMDA Repository</name>
<url>http://team.andromda.org/maven2</url>
</repository>
</repositories>

<pluginrepositories>
<pluginrepository>
<id>andromda</id>
<name>AndroMDA Repository</name>
<url>http://team.andromda.org/maven2</url>
</pluginrepository>
</pluginrepositories>
</project>



Wykonanie komendy

mvn

w katalogu z wyżej opisanym pomem powoduje instalację pluginów mavenowych.

Generowanie aplikacji



Po instalacji pluginów można przystąpić do generowania przykładowej aplikacji, która posłuży jako baza do wykonywania przekształceń.

W tym celu, w katalogu w którym znajduje się plik pom.xml opisany w poprzednim akapicie należy wydać polecenie:


mvn org.andromda.maven.plugins:andromdapp-maven-plugin:3.4-SNAPSHOT:generate


uruchomiony zostanie wizzard, który wymaga od nas podania informacji na temat aplikacji którą chcemy wygenerować z naszego modelu.


INFO [AndroMDA] discovered andromdapp type --> 'j2ee'
INFO [AndroMDA] discovered andromdapp type --> 'richclient'

Please choose the type of application to generate [j2ee, richclient]
j2ee

Please enter the parent directory of your new application directory (i.e. C:/workspaces):
/dane/projekty/sandbox/mda-projekt-eksperymentalny

Please enter your first and last name (i.e. Chad Brandon):
mw

Which kind of modeling tool will you use?
(uml1.4 or uml2 for .xml.zip/.xml/.xmi/.zargo files,
emf-uml22 for .uml files, rsm7 for .emx files) [uml1.4, uml2, emf-uml22, rsm7]:
uml1.4

Please enter the name (maven project description) of your J2EE project (i.e. Animal Quiz):
Projekt eksperymentalny

Please enter an id (maven artifactId) for your J2EE project (i.e. animalquiz):
mdaprojekt

Please enter a version for your project (i.e. 1.0-SNAPSHOT):
1.0-SNAPSHOT

Please enter the root package name (maven groupId) for your J2EE project (i.e. org.andromda.samples.animalquiz):
mw

Would you like an EAR or standalone WAR? [ear, war]:
war

Please enter the type of transactional/persistence cartridge to use (enter 'none' if you don't want to use one) [spring, none]:
spring

Please enter the programming language to be used in service and dao implementations [java, groovy]:
java

Please enter the database backend for the persistence layer [h2, hypersonic, mysql, oracle, db2, informix, mssql, pointbase, postgres, sybase, sabdb, progress, derby]:
hypersonic

Will your project need workflow engine capabilities? (it uses jBPM and Hibernate3)? [yes, no]:
no

Will your project have a web user interface? [yes, no]:
yes

Would you like your web user interface to use JSF or Struts? [jsf, struts]:
jsf

Would you like a standalone or portlet JSF application (Note: Liferay is the only currently supported portlet container)? [standalone, portlet]:
standalone

Would you like to be able to expose your services as web services? [yes, no]:
no

Would you like to use the embedded Jetty web server (Maven plugin)? [yes, no]:
no


Po zakończeniu pracy wizzarda, na dysku tworzony jest projekt mavenowy, który w teorii pozwala nam na wygenerowanie kompletnej aplikacji.

Tu jednak pojawiają się schody i tradycyjnie praktyka bardzo rozchodzi się z teorią. Pojawiają się problemy, które opisane zostały już przez Jacka Laskowskiego.

Chociaż od tamtej chwili minęło już trochę czasu, a AndroMDA doczekała się nowej wersji, to jednak w sprawie starych zależności w repo nie zmieniło się nic i wygeneroany projekt zwyczajnie się nie buduje...

Sam mechanizm przekształceń modelu PIM->PSM stanowiący realizację koncepcji MDA działa bardzo dobrze i daje duże możliwości w zakresie konfigurowania i dopasowywania do indywidualnych potrzeb projektu.

W moim przekonaniu autorzy frameworku przesadzili, chcieli osiągnąć zbyt dużo. Założyli że na podstawie modelu zawsze są w stanie torzyć kompletne, skompilowane aplikację które bez jakiejkolwiek ingerencji w kod da się z sukcesem zdeployować na różnych środowiskach.
Ponieważ pomysł ten nie za bardzo mi się podoba, framework AndroMDA chciałbym wykorzystać w inny sposób, mianowicie włączyć go jako moduł Mavenowy, do zupełnie oddzielnego projektu (Spring Web Flow).
Jego rola ograniczyłaby się do generowania plików źródłowych modelu. Takie zastosowanie zostało przewidziane przez autorów frameworka - świadczy o tym informacja zawarta w artykule A Bird's Eye view of AndroMDA udostępnianym na stronach oficjalnej dokumentacji projektu (zawiera on wiele ciekawych informacji dotyczących głównych założeń frameworka, oraz sposobu jego działania - wiele mi wyjaśnił i bardzo go polecam).

Mam nadzieję, że wkrótce uda mi się wypróbować AndroMDA w praktyce i znaleźć chwilę czasu by o tym napisać :).

Modelowanie aplikacji - czy aby na pewno to zło konieczne?

Ostatnio spotkałem się z twierdzeniem,że tworzenie i utrzymywanie modelu aplikacji jest niepotrzebnym zbytkiem i zwykłą stratą czasu.
Ze względu na fakt, że zagadnienia dotyczące samego modelowania, oraz późniejszego przetwarzania modelu jest mi bardzo bliskie, doszedłem do wniosku że odniesienie się do tej tezy będzie dobrą okazją do odświeżenia swojego bloga.

Nie ulega wątpliwości że przygotowanie kompleksowego projektu wymaga dużego wysiłku, który przekłada się na realny koszt. Czy warto podejmować ten wysiłek i inwestować w niego cenny czas? Zapewne to zależy i to od wielu czynników. W moim odczuciu jest to wręcz konieczność - szczególnie w przypadku dużych projektów, w których realizację zaangażowana jest duża liczba osób. Bez solidnego modelu nie sposób koordynować prac i dbać o "spójność wizji". Jednym tchem mógłbym wymienić jeszcze wiele innych, bezdyskusyjnych zalet wynikających z posiadania modelu aplikacji, ale mimo tego przygotowywanie diagramów umlowych często traktowane jest jako zło konieczne i męczący obowiązek, dlaczego ?

Myślę że składa się na to wiele czynników z których najważniejsze to:

1. Niewłaściwy cel i przeznaczenie modelu - jeśli jedyną motywacją skłaniającą do tworzenia modelu jest przygotowanie wkładu do dokumentacji projektowej to nasz wysiłek nie ma szans się zwrócić. Taki model bardzo szybko straci aktualność. Zespół deweloperski nie będzie posiadał odpowiedniej motywacji do stałego utrzymania spójności pomiędzy diagramami i kodem. Dobry model żyje i zmienia się wraz z aplikacją. Iteracje i przejścia pomiędzy modelem i kodem muszą być wpisane w realizowany cykl wytwórczy, a sam model musi stanowić dla programistów źródło pierwotnej wiedzy o rozwijanej aplikacji.

2. Niewłaściwe narzędzia - ich mnogość potrafi zakręcić w głowie, ale dokonanie niewłaściwego wyboru może skutecznie zniechęcić do tworzenia modelu. Przy wyborze narzędzia ważne jest nie tylko to, jakie diagramy można tworzyć przy jego pomocy ale również to, jak wspiera pracę grupową, oraz czy pozwala na łatwe i bezbłędne przekształcanie modelu w kod aplikacji(i z powrotem). Aspekt łatwego przechodzenia pomiędzy kodem i modelem ma decydujący wpływ na utrzymanie aktulności modelu - a przecież tylko taki model stanowi rzeczywistą wartość.

Jeśli chodzi o wybór narzędzia do modelowania to warto zwrócić uwagę na możliwości jakie dostarcza nam ono w zakresie przekształcania modelu, walidacji i kontroli jego spójności oraz automatyzacji wykonywania różnego rodzaju generat.

Przykładem narzędzia, na które w moim przekonaniu warto zwrócić uwagę, jest Enterprise Architect - komercyjny produkt firmy Sparx Systems. Do celów testowych można skorzystać z darmowego 30 dniowego triala. Niestety nie jest to dużo ale wystarczy by solidnie się pobawić.
Nie chcę tu rozwodzić się na temat tego, co to narzędzie ma, a czego mu brakuje. Chciałbym się natomiast skupić na czymś, co według mnie wyraźnie go wyróżnia.

Mianowicie Sparx Systems, wraz ze swoim narzędziem dostarcza API pozwalające na swobodną manipulację obiektami rozwijanego modelu. Daje to niesamowite możliwości, które właściwie wykorzystane pozwalają na znaczne zwiększenie potencjału narzędzia. Dzięki dostarczanym bibliotekom otrzymujemy pełną obiektową reprezentację naszego modelu. Biblioteki pozwalają nam na przetwarzanie istniejących oraz tworzenie zupełnie nowych elementów. Otrzymujemy możliwość programowego wykonania większości funkcjonalności dostępnej przez GUI EA. Daje to dużą swobodę w zakresie twórczego wykorzystania modelu aplikacji.

Możliwość programowego dostępu do modelu jest to luksus, którego na próżno szukam w świecie Open Source. Duże nadzieję pokładam w Eclipsowym frameworku Eclipse EMF, oaraz porojektach Papyrus i MOSKitt. Niestety nie udało mi się jeszcze przeprowadzić ich rozpoznania.

Mam nadzieję że wkrótce uda mi się przygotować materiał do pokazania praktycznego zastosowania programowego dostępu do repozytorium obiektów modelu, a wtedy na pewno o tym napiszę.

Przydatne skrypty czyli groch z kapustą

Skrypt który umożliwia łatwiejsze przeglądanie logów serwera, zamienia ich kolejność w ten sposób, że ostatnie wpisy (w liczbie 100) pojawiają się na górze pliku.

writer = new File("serverREVERT.log").newWriter("UTF-8", false)
lines=new File("server.log").readLines().asList()

max=lines.size()-1
min=lines.size()-200

for(index in max..min){
writer.writeLine(lines.get(index--))
}
writer.close()


WinTail

Kod wyświetlający na konsoli ostatnie 200 linii z pliku logów serwera mógłby wyglądać tak:

//pathToLogFile="C:\\JAVA\\glassfish\\domains\\domain1\\logs\\server.log"
pathToLogFile="C:\\JAVA\\jboss-4.2.1.GA\\server\\default\\log\\server.log"
linesNumber=100

"cmd /c cls".execute()
lines=new File(pathToLogFile).readLines().asList()
max=lines.size()-1
min=lines.size()-linesNumber
if(min<0)min=0>Można go rozbudować o kilka dodatkowych linii, ktorych zadaniem jest wyswietlenie blokow oznaczonych przez debuggera, oczywiscie sami musimy zadbać o to, by bloki które chcemy wyświetlić były oznaczone w odpowiedni sposób. W moim kodzie blok SQL, który chce monitorować otaczam tagami [SQLDEBUG] i [END-SQLDEBUG]. Skrypt jest wykonywany jesli zmienna DEBUG jest ustawiona na 1.


DEBUG=1
if(DEBUG==1){
pattern=/\[SQLDEBUG\].*\[END-SQLDEBUG\]/

print "\n\n ************************************************************"
print "\n ************** OZNACZONE KOMUNIKATY DEBUGGERA **************"
print "\n ************************************************************\n"
content=new File(pathToLogFile).getText();
content.eachMatch(pattern){ match->
print "\n"+match[0]
}
print "\n ************************************************************\n"
}


Skrypt zmieniający duże litery na małe w danym katalogu


groovy -e "new File(\".\").eachFile{it.renameTo(new File(it.getName().toLowerCase()))}"



Skrypt listujący pliki z drzewa podkatalogów



outF=new File("output.log").newWriter("UTF-8", false)
new File(".").eachFile{
if(it.getName().matches(".*svn.*")||it.getName().matches(".*prop.*")||it.getName().matches(".*base.*"))
return
if (it.isDirectory())
listujKatalog it
}
listujKatalog new File(".")



void listujKatalog(File aKat){
outF.writeLine("--"+aKat.getName())
aKat.eachFile{
if(it.getName().matches(".*doc.*")){
outF.writeLine(it.getName())
}
}
}
outF.close()

Rekurencyjne przetwarzanie drzewa katalogów

Często okazuje się że istnieje potrzeba wykonania jakiejś operacji na wszystkich zbiorach które znajdują się na dysku i umieszczone są w różnych katalogach, jako elementy potomne mające wspólny korzeń. Wtedy wykorzystać można bardzo wygodny mechanizm przetwarzania rekurencyjnego. Jako przykład niech posłuży skrypt, który z drzewa katalogów usuwa wszystkie katalogi (wraz z zawartością) .svn zawierające pliki związane z obsługą svn.


import org.apache.commons.io.FileUtils

def basedir="kat1\\kat2" //SCIEZKA_DO_ROOT

outputLog=new File("output.log").newWriter("UTF-8", false)

dir = new File(basedir)
fileList=[]
dir.eachFileRecurse{ f ->
if((f.name=='.svn')&&(f.isDirectory())&&(f.exists())){
fileList.add(f)
}
}


for(mf in fileList){
outputLog.writeLine( "Usunieto: "+mf.canonicalPath)
FileUtils.deleteDirectory(mf)
}

outputLog.writeLine( "Calkowita liczba usunietych:"+fileList.size())
outputLog.close()

Do poprawnego działania konieczne jest sciągnięcie biblioteki commons.io.*, a następnie umieszczenie pliku commons-io-xxxxx.jar w katalogu GROOVY_HOME\lib

Groovy a wyrażenia regularne

Wprowadzenie


Teraz gdy już przebrnąłem przez podstawowe metody WE/WY zacząłem kombinować jak wykorzystać Grooviego do przetwarzania plików tekstowych. A wiadomo, że gdy mamy na myśli język skryptowy i ocenę jego przydatności we wspomnianym zakresie to koniecznie należy przyjrzeć się w jaki sposób obsługuje on wyrażenia regularne.

Wydaje mi się że o mocy która kryje się w wyrażeniach regularnych nikogo przekonywać nie trzeba. Naprawdę nie mogę zrozumieć, dlaczego tak mało edytorów tekstu tworzonych pod okna wspiera i wykorzystuje ten potężny mechanizm wyszukiwania i przetwarzania wzorców. Ale do rzeczy.

Do przetwarzania wyrażeń regularnych Groovy wykorzystuje biblioteki dostarczane z JDK. Oczywiście wprowadza szereg udogodnień, które sprawiają, że przeszukiwanie tekstów pod kątem występowania w nich jakiegoś wzorca jest dużo prostsze i przyjemniejsze niż w samej Javie.
Dzięki mechanizmom wyrażeń regularnych Groovy pozwala na:
  • określenie czy (a jeśli tak to ile razy) w tekście wystąpił ciąg znaków zgodny ze wzorcem
  • wykonanie określonych operacji na dopasowanym łańcuchu znaków
  • dokonanie zastąpień dopasowanych łańcuchów znakowych
  • podział dopasowanego ciągu na podciągi

Definiowanie wzorców



Przetwarzanie tekstów z wykorzystaniem wyrażeń regularnych wiąże się z koniecznością definiowania wzorców dopasowań. Są to instrukcje dla mechanizmu przetwarzającego zawierające informacje czego ma szukać i w jaki sposób to przetwarzać. Mają one postać ciągów znakowych, zawierających pewne symbole o specjalnym znaczeniu. Dzięki nim możemy określić pewne grupy znaków, liczb, które mają zostać wyszukane w tekście. Najważniejsze z nich to:

  • . (kropka) - dowolny znak
  • ^ - początek linii
  • $- koniec linii
  • \d- dowolna liczba
  • \D- dowolny znak nie będący liczbą
  • \s- biały znak
  • \S- dowolny znak nie będący znakiem białym
  • \w- słowo
  • \W-znaki nie układające się w słowa
  • \b-granica słowa
  • ()-operator grupowania
  • (a|b)-warunek logiczny "lub" (a lub b)
  • * - zero lub więcej wystąpień poprzedzającego znaku(grupy)
  • + - jeden lub więcej wystąpień poprzedzającego znaku(grupy)
  • ? - dokładnie jedno wystąpienie poprzedzającego znaku(grupy)
  • x{m,n} - co najmniej m i co najwyżej n znaku(grupy) poprzedzajacej
  • x{m} - dokladnie m wystapien znaku (grupy) poprzedzajacej
Wzorce dopasowań, rozpoznawane przez Grooviego ograniczane są znakami "/"(slash). Przykładowe definicje masek wyglądają następująco:
  • /\d\d-\d\d-\d\d\s?\d\d:\d\d/ lub w uproszczeniu \d{2}-\d{2}-\d{2}\s?\d{2}:\d{2} - dopasuje się do wszystkich dat zapisanych w formacie "liczba liczba-liczba liczba- liczba liczba SPACJA liczba liczba:liczba liczba", czyli np. 01-02-07 10:22
  • /href=".*?"/ - maska pozwalająca na wyszukanie w tekście wystąpień adresów URL
Pod adresem http://www.nvcc.edu/home/drodgers/ceu/resources/test_regexp.asp znaleźć można proste narzędzie, które służy do testowania wrażeń regularnych.

Dopasowanie zachłanne

Operatory rozszerzające (takie jak * i +) mają pewną cechę, o której zawsze należy pamiętać. Chodzi o to, że dążą one do dopasowania jak najszerszego możliwego ciągu znakowego pasującego do zadanego wzorca.

Spójrzmy na następującą maskę: /href=".*"/.
Należy rozumieć ją następująco:
dopasuj się do ciągu znaków, rozpoczynającego się słówkiem "href", po którym występuje znak "=" i cudzysłów ("), w jego wnętrzu ma znaleźć się zero lub więcej dowolnych znaków kończących się cudzysłowem.

Wydawać by się mogło, że taki wzorzec prawidłowo wyciągnie nam wszystkie adresy stron w postaci href="adres". Tak rzeczywiście by było gdyby nie zachłanność dopasowania operatora *.
Problem występuje w sytuacji gdy wyrażenie regularne zostanie zastosowane np. do następującego tekstu:

To jest (href="www.pierwszy.adres.url") pierwszy rozdzial (href="www.drugi.adres.url")

Dopasowanie nastąpi w następujący sposób: znaleziony zostanie następujący ciąg znaków: href=", i jak dotąd wszystko jest zgodne z oczekiwaniami, jednak potem zaczynają się problemy, bo każdy znak, który pojawi się potem, aż do napotkania OSTATNIEGO cudzysłowu zostanie uznane jako pasujące do ".*", a więc wynikiem zastosowania reguły wzorca będzie ciąg znaków:

href="www.pw.edu.pl") pierwszy rozdzial (href="www.bla.bla.pl"

Czy o to mi chodziło ? Niekoniecznie....

SPOSÓB OGRANICZENIA ZACHŁANNOŚCI:

Aby ograniczyć zachłanność operatora, należy we wnętrzu reguły, bezpośrednio po znaku "*", a przed znakiem ograniczającym zakres dopasowania (u nas cudzysłów) znaku "?". Spowoduje to dopasowanie się nie do maksymalnie najdłuższego łańcucha, tylko do najkrótszego.

A więc nasza reguła wybierająca powinna wyglądać następująco: /href=".*?"/. Wynik jej działania jest już zgodny z naszymi oczekiwaniami :

href="www.pierwszy.adres.url"
href="www.drugi.adres.url"

Wykorzystywanie wyrażeń regularnych


Metody find() i matches()
Wyrażenia regularne w Groovym obsługiwane są przez klasę java.util.regex.Matcher, która udostępnia dwie bardzo użyteczne metody- find() i matches().
  • matcher.find()- pozwala na określenie czy w określonym ciągu znakowym, da się wyszukać podciąg zgodny ze wzorcem dopasowania
  • matcher.matches()- przeszukuje łańcuch wejściowy i próbuje dopasować cały wzorzec
Dla uproszczenia Groovy definiuje dwa pomocne operatory:
  • =~ jest tożsamy z uruchomieniem metody matcher.find()
  • ==~ - jest tożsamy z uruchomieniem metody matcher.matches()
Metoda find() zwraca obiekt matcher. Aby dostać się do wszystkich zwróconych przez nią obiektów można wywołać jej metodę each() - wraz z zastosowaniem domknięcia. Przykładem działania niech będzie skrypt wyszukujący w łańcuchu znaków wystąpień cyfry 4.

text='1 2 4 8 64'
(text=~/4/).each{match->print match}

(text=~/4/) - zwraca obiekt matcher, który zawiera w sobie dopasowane ciągi znaków ( w tym przypadku wystąpienia znaku 4)

Metoda String.eachMatch(regex)Alternatywnym i w moim przekonaniu bardziej intuicyjnym sposobem dotarcia do obiektów otrzymanych w wyniku zastosowania wzorca, jest metoda String.eachMatch(wyrazenie_regularne).
Dzięki niej i wykorzystaniu mechanizmu domknięć możliwe jest przetworzenie każdego podzbioru znaków, który na podstawie wzorca wyrazenie_regularne zostani wyodrębniony z obiektu String, na którym wywoływana jest metoda. Np.

pattURL=/href=".*?"/
content=new File("regularExp_source.txt").getText();
content.eachMatch(pattURL){ match->
print "\n"+match[0]
}

Powyższy skrypt powoduje że w pliku regularExp_source.txt wyszukane zostaną wszystkie adresy url. Należy zwrócić uwagę na ciekawą rzecz. Mianowicie obiekt matcher wyniki dopasowania zwraca nie w postaci pojedynczego łańcucha znaków, lecz tablicy, pod której zerowym indeksem znajduje się wynik dopasowania. Jeśli we wzorcu wystąpiło n operatorów grupowania () to każda z dopasowanych grup znajdzie swe miejsce pod kolejnym indeksem.

Metoda String.replaceAll(regex)
Inną ciekawą metodą, jest String.replaceAll(regex). Pozwala ona na zamianę jednych ciągów znakowych na inne.

Schemat jej działania jest następujący:
content.replaceAll(/wzorzec/){it - 'ciągZastępowany'+'ciągZastępujący'}. Aby zamiana nastąpiła ciągZastępowany musi zostać dopasowany przez wzorzec.

Przykładem zastosowania jest skrypt zamieniający polskie znaki na ich liczbowe reprezentacje w utf.

myFile=new File("znakiUTF.txt") myFile.write("\n")
content=new File("znakiAscii.txt").text
content=content.replaceAll(/Ą/){it - 'Ą'+'\\\\u0104'}
content=content.replaceAll(/ą/){it - 'ą'+'\\\\u0105'}
content=content.replaceAll(/Ć/){it - 'Ć'+'\\\\u0106'}
content=content.replaceAll(/ć/){it - 'ć'+'\\\\u0107'}
content=content.replaceAll(/Ę/){it - 'Ę'+'\\\\u0118'}
content=content.replaceAll(/ę/){it - 'ę'+'\\\\u0119'}
content=content.replaceAll(/Ł/){it - 'Ł'+'\\\\u0141'}
content=content.replaceAll(/ł/){it - 'ł'+'\\\\u0142'}
content=content.replaceAll(/Ó/){it - 'Ó'+'\\\\u00D3'}
content=content.replaceAll(/ó/){it - 'ó'+'\\\\u00F3'}
content=content.replaceAll(/Ś/){it - 'Ś'+'\\\\u015A'}
content=content.replaceAll(/ś/){it - 'ś'+'\\\\u015B'}
content=content.replaceAll(/Ź/){it - 'Ź'+'\\\\u0179'}
content=content.replaceAll(/ź/){it - 'ź'+'\\\\u017A'}
content=content.replaceAll(/Ż/){it - 'Ż'+'\\\\u017B'}
content=content.replaceAll(/ż/){it - 'ż'+'\\\\u017C'}
myFile.write(content,"UTF-8")

Operacje odczytu i zapisu

Obsługa plików tekstowych


Przetwarzanie plików tekstowych i obsługa wyrażeń regularnych jest jednym z najczęstszych zastosowań języków skryptowych, dlatego zgłębianie możliwości grooviego rozpocząłem od obsługi operecji wejścia/wyjścia.

Odczyt pliku

W najprostszym przypadku odczyt pliku można wykonać w jednej linii kodu. Ma on następującą postać:

new File("source.txt").eachLine{linia -> print linia}

W powyższym przykładzie wykorzystałem powszechnie stosowany mechanizm domknięć (clousures). Polega on na tym że metoda przekazuje swój wynik do operacji które znajdują się w zasięgu nawiasów klamrowych. Dostęp do wartości zwróconej przez metodę uzyskuje się przy pomocy odczytu wartości parametru zdefiniowanego na samym początku domknięcia, przed znakiem "->" - w tym przypadku "linia". Powyższy kod po przeczytaniu każdej z linii pliku wyświetli ją na konsoli. Dokumentacja języka mówi, że nie musimy się martwić o to, że podczas odczytu takiego pliku może wystąpić wyjątek. Mechanizmy obsługi plików zadbają o to by został on właściwie obsłużony, a źródło prawidłowo zamknięte.

Często zdarza się, że nie istnieje konieczność przetwarzania kolejnych linii pliku. Wtedy można użyć metod wczytujących do zmiennych tekstowych całą tekstową zawartość pliku. Skrypt dokonujący tej czynności wygląda następująco:

content=new File("source.txt").text
print content

Wczytuje on do zmiennej "content" całą zawartość pliku source.txt, a potem wyświetla ją na ekranie.

W sytuacji, gdy zaistnieje potrzeba odczytu pliku o niestandardowym kodowaniu, wtedy można posłużyć się metodą getText(encoding).

content=new File("source.txt").getText("UTF-8")
print content

Zapis do pliku

W przypadku gdy chcemy jednokrokowo zapisać całą zawartość pliku tekstowego, mamy do dyspozycji metodę:

new File("output.txt").write("hello :)")

W przypadku, gdy zachodzi konieczność obsługi kodowania, można podać je jako dodatkowy parametr metody:

new File("output.txt").write("hello :)", "UTF-8")

Jeżeli chcielibyśmy zapisywać tekst całymi liniami można skorzystać z następującego sposobu:

writer = new File("myText.txt").newWriter("UTF-8", true)
writer.writeLine("pozdrowienia")
writer.close()

Dodatkowy parametr w metodzie pobierającej obiekt Writer decyduje o tym czy plik ma zostać założony lub wyczyszczony(false), czy też treść ma być do niego dopisana(true).


Należy pamiętać że otwarcie pliku do zapisu zakłada nowy plik, lub kasuje jego starą zawartość. Jeżeli zaistnieje konieczność dopisywnia tekstu do istniejącego pliku można skorzystać z metody append():

new File("output.txt").append("hello :) - po raz drugi")

tak samo jak w poprzednich sytuacjach, opcjonalnie można określić kodowanie pliku:

new File("output.txt").append("hello :) - po raz drugi","utf-8")

Obsługa plików binarnych

Sposoby obsługi plików binarnych, oraz inne metody stosowane przy zapisie i odczycie plików szczegółowo omówione zostały pod adresem http://groovy.codehaus.org/Streams,+Readers,+and+Writers