Nowy projekt to zwykle dobry czas na przejrzenie różnych technologii. Szablony tekstowe w postaci StringTemplate czekały sobie już dobre parę lat na półce do przetestowania. I tak czekał i czekał i … I nigdy nie było dobrej okazji, żeby z niego skorzystać. Aż do dziś.
Szablony tekstowe?
Każdy programista z szablonami tekstowymi spotykał się nie raz. Prosty przykład:
Welcome <name> to the World of <world>
Generalnie problem polega na tym, ze mamy pewien szablon tekstu i chcielibyśmy, żeby ten szablon został wypełniony naszymi zmiennymi. W powyższym przykładzie fajnie by było, gdybyśmy mogli podstawiać sobie zamiast name i world nasze wartości. Takie podstawienie nazywamy interpolacją tekstu (ang. string interpolation).
Nasuwają się pytania. Dlaczego tego potrzebujemy? Czy nie wystarczy np. konkatenacja Stringów, czy też skorzystanie z metody String.format(…)?
Odpowiedź – oczywiście czasem wystarczy, a czasem nie. A gdy przyjdzie nam się spotkać z jakimś dużym szablonem, np. stroną HTML, to … zdecydowanie nie wystarczy. Nie będzie to ani estetyczne, ani efektywne do kodowania. A nasz kod będzie wyglądał raczej jak puszka pandory.
StringTemplate na ratunek
Narzędzia do prostych problemów powinny być proste. I właśnie taki jest StringTemplate. Zobaczmy kilka przykładów.
Przykład #1 Chcielibyśmy szybko podstawić atrybut language w szablonie. Nic prostszego. Tworzymy szablon ST i dodajemy do niego wartość naszego atrybutu language za pomocą add(). Wynik podstawienia otrzymujemy za pomocą metody render().
@Test
public void simpleInterpolation() {
ST template = new ST("Ready for <language> programming language!");
template.add("language", "Java");
String result = template.render();
assertEquals("Ready for Java programming language!", result);
}
Przykład #2 Tym razem chcielibyśmy przekazać do silnika szablonów bardziej złożony obiekt. Params zawiera dwa pola, tj. left i right. Przekazujemy ten obiekt jako atrybut obj i StringTemplate zrobi już resztę jak w przykładzie #1.
@Test
public void interpolationWithParamsObject() {
Params params = new Params("Left", "Right");
ST template = new ST("Ready for <obj.left> and <obj.right>");
template.add("obj", params);
String result = template.render();
assertEquals("Ready for Left and Right", result);
}
public class Params {
private String left;
private String right;
public Params(String left, String right) {
this.left = left;
this.right = right;
}
public String getLeft() {
return left;
}
public String getRight() {
return right;
}
}
Przykład #3 Załóżmy tym razem, że chcielibyśmy nasze szablony trzymać oddzielnie, nie wpisywać ich do kodu. W typowym projekcie mamy katalog resources. W nim możemy dodać sobie katalog templates, do którego wrzucimy plik welcome.st:
welcome(name, world) ::= "Welcome <name> to the World of <world>"
Powyżej mamy formalną definicję szablonu welcome, który przyjmuje dwa atrybuty name i world. Zobaczmy, jak można z tego skorzystać w kodzie:
@Test
public void classPathNamedTemplate() {
STGroup group = new STGroupDir("templates");
ST classPathNamedTemplate = group.getInstanceOf("welcome");
classPathNamedTemplate.add("name", "Neo");
classPathNamedTemplate.add("world", "Matrix");
String result = classPathNamedTemplate.render();
assertEquals("Welcome Neo to the World of Matrix", result);
}
Pomaga nam klasa STGroupDir. Dzięki niej wczytywane są szablony leżące w podkatalogu templates w classpath (czyli typowo katalog projektowy resources/templates).
Kiedy mamy taką grupę szablonów zaczytaną, wówczas możemy posłużyć się jej motodą getInstanceOf i pobrać interesujący nas szablon. Dalej operacje przebiegają podobnie, jak w poprzednich przykładach.
Przykład #4 Ok, ja zwykle nie jestem takim formalistą jeśli chodzi o szablony i nie chcę korzystać z tej formalnej definicji, jak w przykładzie #3. Wolę sam kod szablonu. Tym razem niech będzie to raw-welcome-to-template.st:
Welcome <name> to the World of <world>
Różnica podstawowa, nie ma nazwy szablonu i listy argumentów na początku. Jest sam szablon, który nas interesuje.
@Test
public void classPathRawTemplate() {
STGroupDir group = new STRawGroupDir("templates");
ST classPathNamedTemplate = group.getInstanceOf("raw-welcome-to-template");
classPathNamedTemplate.add("name", "Gandalf");
classPathNamedTemplate.add("world", "Moria");
String result = classPathNamedTemplate.render();
assertEquals("Welcome Gandalf to the World of Moria", result);
}
I tu możemy zastosować STRawGroupDir. Dzięki tej klasie nazwa naszego szablonu nazywa się jak plik, w którym się znajduje. Reszta operacji podobna, jak wcześniej. Prawda, że proste?
Co jeszcze może StringTemplate
StringTemplate jest prosty, co widać w powyższych przykładach. To wcale nie oznacza, że nie ma również trochę bardziej zaawansowanych funkcji.
Jeśli przyjrzymy się liście, to zauważymy, że jest w nim trochę możliwości:
- Mamy do dyspozycji wiele wyrażeń, np. wczytywanie innych szablonów w obecnie procesowanym.
- Zawiera w sobie kilka funkcji pomocniczych, np. sprawdzenie długości atrybutu tekstowego, pobieranie pierwszego elementu z listy.
- Dostępne są instrukcje warunkowe, np. if, elseif, else.
- Jest również wbudowane grupowanie szablonów.
Alternatywne rozwiązania
Jest oczywiście kilka alternatyw. Oto kilka z nich:
- Velocity Jest to chyba pierwszy silnik szablonów, z którym się zetknąłem w Javie. Osobiście uważam, że jest już tak stary, że nie powinno się go używać. Reasumując nie polecam.
- Freemarker pozwala na dużo. Mi się nigdy jakoś szczególnie nie podobał.
- Thymeleaf jest bardzo przyjemnym rozwiązaniem do większych zastosowań i bez większych problemów integruje się ze Springiem.
- Groovy Markup Templates, jeśli przy okazji korzystasz z Groovy, też fajnie współpracuje ze Springiem.
Podsumowanie
Jeśli czytałeś Java JDK 15 – co nowego? to wiesz, że w Java 15 pojawią się już na stałe String Blocks, czyli wielowierszowe Stringi. W języku nie ma jednak prostego narzędzia do obsługi szablonów tekstowych.
I tu jest m. in. miejsce dla StringTemplate, który wpasowuje się idealnie i pozwala na proste szablony tekstowe.
Daj znać, jak Ci się podoba praca ze StringTemplate. Czekam na Twój komentarz 😀