Оглавление

4 Стандарт синтаксиса Выражений/Standard Expression Syntax


Мы сделаем небольшой перерыв в развитии нашего виртуального магазина бакалейных товаров, чтобы узнать об одной из наиболее важных частей Стандартного диалекта Thymeleaf: Стандарте синтаксиса выражений Thymeleaf.

Мы уже видели два типа допустимых значений атрибутов, выраженные в этом синтаксисе: сообщения и переменные:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

<p>Today is: <span th:text="${today}">13 february 2011</span></p>

Но существует значительно больше выражений. Первое — сделаем краткий обзор Standard Expression:

  • Простые выражения:
    • Переменная: ${...}
    • Выбранная переменная: *{...}
    • Сообщение: #{...}
    • Ссылка URL: @{...}
    • Фрагмент: ~{...}
  • Литералы/Literals:
    • Текст: 'one text', 'Another one!',...
    • Число: 0, 34, 3.0, 12.3,...
    • Boolean: true, false
    • Null: null
    • Токены: one, sometext, main,...
  • Текст:
    • Соединение строк: +
    • Подстроки: |The name is ${name}|
  • Арифметика:
    • Binary: +, -, *, /, %
    • Минус (unary operator): -
  • Boolean:
    • Binary: and, or
    • Boolean отрицание (unary operator): !, not
  • Сравнение и равенство:
    • Сравнение: >, <, >=, <= (gt, lt, ge, le)
    • Равенство: ==, != (eq, ne)
  • Условные:
    • If-then: (if)? (then)
    • If-then-else: (if)? (then): (else)
    • Default: (value) ?: (defaultvalue)
  • Специальные токены:
    • No-Operation: _

Выражения могут комбинироваться и вкладываться:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

4.1 Сообщения/Messages


Как мы знаем, выражение сообщения #{...} позволяет нам связывать:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

… с:

home.welcome=?Bienvenido a nuestra tienda de comestibles!

Но есть один аспект, о котором мы еще не подумали: что произойдет, если текст сообщения не будет полностью статическим? Что, если, например, наше приложение знает, какой пользователь посещает сайт, и мы хотели бы приветствовать пользователя по имени?

<p>?Bienvenido a nuestra tienda de comestibles, John Apricot!</p>

Это означает, что нам необходимо добавить параметр в наше сообщение. Вот так:

home.welcome=?Bienvenido a nuestra tienda de comestibles, {0}!

Параметр определяется относительно стандартного синтаксиса java.text.MessageFormat, который означается, что вы можете форматировать числа и даты.

Чтобы указать значение для нашего параметра из атрибут HTTP сеанса, называемого пользователем, мы напишем:

<p th:utext="#{home.welcome(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

Несколько параметров можно определить, разделив запятыми. Так же сами ключи сообщения могут прийти из переменной:

<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

4.2 Переменные/Variables


Мы уже упоминали, что выражения ${...} на самом деле являются выражениями OGNL (Object-Graph Navigation Language), выполненными на map переменных, содержащихся в контексте.

Для получения подробной информации о синтаксисе и функциях OGNL вы должны прочитать «Руководство по языку OGNL».

В Spring приложениях с поддержкой MVC OGNL будет заменен на SpringEL, но его синтаксис очень похож на синтаксис OGNL (фактически, точно такой же для большинства распространенных случаев).

Из синтаксиса OGNL мы знаем, что выражение:

<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>

эквивалентно:

ctx.getVariable("today");

Но OGNL позволяет создавать более мощные выражения, как это:

<p th:utext="#{home.welcome(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

… получаем имя пользователя:

((User) ctx.getVariable("session").get("user")).getName();

Но getter — это лишь одна из особенностей OGNL. Посмотрим еще:

/*
 * Доступ к свойствам с использованием точки (.). Эквивалент вызывающим getter свойств.
 */
${person.father.name}

/*
 * Доступ к свойству может быть так же через скобки ([]) и написание
 * имени свойства как переменной или между простыми кавычками.
 */
${person['father']['name']}

/*
 * Если объект является map, оба синтаксиса точки и скобки будет эквивалентен 
 * выполнению метода get(...).
 */
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}

/*
 * Доступ к массиву или коллекции так же выполняется через скобки, 
 * с написанием индекса без скобок.
 */
${personsArray[0].name}

/*
 * Могут быть вызваны методы даже с аргументами.
 */
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}

Expression Basic Objects

При выполнении выражения OGNL с переменными контекста некоторые объекты становятся доступными для большей гибкости. На эти объекты можно ссылаться (по стандарту OGNL), начиная с символа #:

#ctx: контекст.
#vars: переменные контекста.
#locale: локаль контекста.
#request: (только в Web Contexts) объект HttpServletRequest.
#response: (только в Web Contexts) объект HttpServletResponse.
#session: (только в Web Contexts) объект HttpSession.
#servletContext: (только в Web Contexts) объект ServletContext.

Так же мы можем делать следующее:

<span th:text="${#locale.country}">US</span>

Вы можете прочитать полные ссылки на эти объекты в Приложении A.

Утилиты выражений

Помимо этих основных объектов, Thymeleaf предлогает нам набор полезных объектов, которые помогут нам выполнять общие задачи в наших выражениях.

#execInfo: информация о текущем шаблоне.
#messages: методы для получения сообщений внутри выражений, так же, как они будут получены с использованием синтаксиса #{...}.
#uris: методы для экранирования частей URL/URI.
#conversions: методы для выполнения настроек службы преобразования (если присутствуют).
#dates: методы для java.util.Date objects: форматирование, извлечение компонентов и тп.
#calendars: аналогично #dates, но для java.util.Calendar объектов.
#numbers: методы для форматирования числовых объектов.
#strings: методы для String объекто: contains, startsWith, prepending/appending и тп.
#objects: общие методы для объектов.
#bools: методы для булевых преобразований.
#arrays: методы для массивов.
#lists: методы для List.
#sets: методы для Sets.
#maps: методы для Maps.
#aggregates: методы для аггрегирования массовов или коллекций.
#ids: методы для обработки идентификаторов id, которые могут быть повторены (например, в результате итерации).

Вы можете проверить, какие функции предлагаются для каждого из этих объектов в Приложении B.

Переформатирование даты на странице Home

Теперь мы знаем об этих утилитах объектов и можем использовать их, чтобы изменить способ отображения даты на нашей домашней странице. Вместо этого в нашем HomeController:

SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar cal = Calendar.getInstance();

WebContext ctx = new WebContext(request, servletContext, request.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));

templateEngine.process("home", ctx, response.getWriter());

… мы можем сделать следующее:

WebContext ctx = 
    new WebContext(request, response, servletContext, request.getLocale());
ctx.setVariable("today", Calendar.getInstance());

templateEngine.process("home", ctx, response.getWriter());

… и затем выполнить форматирование даты в самом слое представления:

<p>
  Today is: <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 May 2011</span>
</p>

4.3 Выражения выбора/selections (синтаксис звездочки)


Выражения с переменными могут быть записаны не только через ${...}, но и как *{...}.

Между вариантами существует разница: синтаксис со звездочкой преобразует выражение над выбранным объектом нежели над всем контекстом. Это значит, что если нет выбранного объекта, доллар и звездочка делают одно и то же.

Что такое выбранный объект? Результат выражения с использованием атрибута "th:object". Используем его на странице профиля (userprofile.html):

<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

Что эквивалентно:

<div>
  <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

Конечно, доллар и звездочка могут смешиваться:

<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

Когда выделенный объект находится на месте, выбранный объект будет так же доступен с долларом, как и #object переменная:

<div th:object="${session.user}">
  <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

Как сказано, если нет выбранного объекта, доллар и звездочка эквиваленты.

<div>
  <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>

4.4 Link URL


Из-за своей важности URL-адреса являются первоклассными «гражданами» в шаблонах веб-приложений, а Стандартный диалект Thymeleaf имеет для них специальный "@" синтаксис: @{...}

Существуют разные типы URL:

  • Абсолютные URLs: www.thymeleaf.org
  • Относительные URLs:
    • Относительные на странице: user/login.html
    • Относительные контекста: /itemdetails?id=3 (контекстное имя будет добавлено на сервере)
    • Относительные сервера: ~/billing/processInvoice (позволяют вызывать URLs в другом контексте (= application) на том же сервере.
    • Относительные протокола: //code.jquery.com/jquery-2.0.3.min.js

Реальная обработка этих выражений и их преобразование в URL-адреса, которые будут выводиться, выполняются с помощью реализации интерфейса org.thymeleaf.linkbuilder.ILinkBuilder, которые регистрируются в используемом объекте ITemplateEngine.

По умолчанию одна реализация этого интерфейса представлена классом org.thymeleaf.linkbuilder.StandardLinkBuilder, которого достаточно как для автономных (не веб-сайтов), так и для веб-сценариев на основе API-интерфейса Servlet. Другие сценарии (например, интеграция с веб-фреймворками, не относящимися к ServletAPI), возможно, потребуют конкретных реализаций интерфейса компоновщика ссылок (link builder interface).

Давайте используем новый синтаксис. Встречайте атрибут "th:href":

<!-- Создаем 'http://localhost:8080/gtvg/order/details?orderId=3' (плюс реврайт) -->
<a href="details.html" 
   th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>

<!-- Создаем '/gtvg/order/details?orderId=3' (плюс реврайт) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>

<!-- Создаем '/gtvg/order/3/details' (плюс реврайт) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

Несколько заметок:

  • "th:href" — это модифицирующий атрибут: однажды выполнившись, он вычислит URL ссылку и установит значение атрибуту «href» тега <a>.
  • Нам разрешено использовать выражения для параметров URL (как вы можете видеть в orderId=${o.id}. Также будут автоматически выполняться требуемые операции кодирования URL-параметров.
  • Если требуется несколько параметров, они будут разделены запятыми: @{/order/process(execId=${execId},execType='FAST')}
  • Переменные шаблоны также допускаются в URL-адресах: @{/order/{orderId}/details(orderId=${orderId})}
  • Относительные URL-адреса, начинающиеся с / (например: /order/details), будут с автоматически префиксами в виде имени контекста приложения.
  • Если файлы cookie не включены или это еще не известно, суффикс ";jsessionid=..." может быть добавлен в относительные URL-адреса, чтобы session была сохранена. Это называется URL Rewriting и Thymeleaf позволяет подключать собственные фильтры перезаписи, используя механизм response.encodeURL(...) из API сервлета для каждого URL-адреса.
  • Атрибут "th:href" позволяет нам (необязательно) иметь действующий статический атрибут href в нашем шаблоне, чтобы наши ссылки на шаблоны оставались навигационными для браузера при открытии напрямую для целей прототипирования.
  • Атрибут "th:href" позволяет нам (необязательно) иметь действующий статический атрибут href в нашем шаблоне, чтобы наши ссылки на шаблоны оставались навигационными для браузера при открытии напрямую для целей прототипирования.

Как и в случае синтаксиса сообщения (#{...}), URL также могут быть результатом выполнения другого выражения:

<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>

Меню для нашей домашней страницы

Сейчас мы знаем, как создать ссылки URLs, что насчет добавления небольшого навигационного меню на нашу Home страницу?

<p>Please select an option</p>
<ol>
  <li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
  <li><a href="order/list.html" th:href="@{/order/list}">Order List</a></li>
  <li><a href="subscribe.html" th:href="@{/subscribe}">Subscribe to our Newsletter</a></li>
  <li><a href="userprofile.html" th:href="@{/userprofile}">See User Profile</a></li>
</ol>

Ссылки URLs относительно корневой директории сервера

Дополнительный синтаксис может использоваться для создания URL-адресов, основанных на корневом каталоге (вместо контекстно-зависимых) URL-адресов, чтобы ссылаться на разные контексты на одном сервере. Эти URL-адреса будут указаны как @{~/path/to/something}

4.5 Фрагменты/Fragments


Фрагментные выражения — это простой способ представить фрагменты разметки и перемещать их между шаблонами. Это позволяет нам копировать, передавать их другим шаблонам в качестве аргументов и тп.

Наиболее распространенное использование — для вставки фрагмента с использованием th:insert или th:replace (подробнее об этом в следующем разделе):

<div th:insert="~{commons :: main}">...

Но они могут использоваться везде, как и любая другая переменная:

<div th:with="frag=~{footer :: #main/text()}">
  <p th:insert="${frag}">
</div>

Позже в этом учебном курсе есть целый раздел, посвященный Template Layout, включая более глубокое объяснение фрагментных выражений.

4.6 Литералы/Literals


Текстовые литералы

Текстовые литералы — это только символьные строки, заданные между одинарными кавычками. Они могут включать любой символ, но вы должны избегать каких-либо одиночных кавычек внутри них, используя \'.

<p>
  Now you are looking at a <span th:text="'working web application'">template file</span>.
</p>

Числовые литералы

Числовые литералы — это просто числа.

<p>The year is <span th:text="2013">1492</span>.</p>
<p>In two years, it will be <span th:text="2013 + 2">1494</span>.</p>

Булевые/Boolean литералы

Булевые литералы — это true и false. Для примера:

<div th:if="${user.isAdmin()} == false"> ...

В этом примере "== false" написаны вне скобок, то о выражении заботится Thymeleaf. Если бы равенство содержалось внутри скобок — о нем бы заботился OGNL/SpringEL движок:

<div th:if="${user.isAdmin() == false}"> ...

Литерал null

Литерал null так же может быть использован:

<div th:if="${variable.something} == null"> ...

Литеральные токены

Числовые, булевые и null литералы являются частным случаем литеральных токенов.

Эти токены позволяют немного упростить Standard Expressions. Они работают точно так же, как текстовые литералы ('...'), но допускают только буквы (A-Z и a-z), цифры (0-9), скобки ([ и ]), точки (.), дефисы (-) и подчеркивания (_). Никаких пробелов, никаких запятых и тп.

Хорошая часть? Токены не нуждаются в кавычках, окружающих их. Поэтому мы можем сделать:

<div th:class="content">...</div>

вместо:

<div th:class="'content'">...</div>

4.7 Appending texts


Тексты, независимо от того, являются ли они литералами или результатом обработки переменных или сообщений, могут быть легко добавлены с помощью оператора +:

<span th:text="'The name of the user is ' + ${user.name}">

4.8 Литералы замены


Литеральные замены позволяют легко форматировать строки, содержащие значения из переменных, без необходимости добавлять литералы с помощью '...' + '...'.

Эти замены должны быть окружены вертикальными черточками (|), например:

<span th:text="|Welcome to our application, ${user.name}!|">

Что эквивалентно:

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

Литеральные замены могут быть объединены с другими типами выражений:

<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">

Только выражения переменной / сообщения (${...}, *{...}, #{...}) разрешены внутри |...| литеральных замен. Никакие другие литералы ('...'), boolean/numeric токены, условные выражения и тп.

4.9 Арифметические операции


Некоторые арифметические операции так же доступны: +, -, *, /, %.

<div th:with="isEven=(${prodStat.count} % 2 == 0)">

Обратите внимание, что эти операторы также могут быть применены непосредственно в самих выражениях OGNL (и в этом случае будет выполняться OGNL вместо механизма Thymeleaf Standard Expression):

<div th:with="isEven=${prodStat.count % 2 == 0}">

Обратите внимание, что для некоторых из этих операторов существуют текстовые псевдонимы: div (/), mod (%).

4.10 Сравнения и равенство


Значения в выражениях можно сравнить с символами >, <, >= и <=, ==, != операторы используются для проверки равенства (или его отсутствия). Обратите внимание, что XML устанавливает < и > символы не должны использоваться в значениях атрибутов, и поэтому они должны быть заменены на &lt; и&gt;.

<div th:if="${prodStat.count} > 1">

<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">

Более простая альтернатива может заключаться в использовании текстовых псевдонимов, существующих для некоторых из этих операторов: gt (>), lt (<), ge (>=), le (<=), not (!). Так же eq (==), neq/ne (!=).

4.11 Условные выражения


Условные выражения предназначены для выполнения только одного из двух выражений в зависимости от результата оценки условия (которое само является другим выражением).

Давайте посмотрим на фрагмент примера (введение другого модификатора атрибута, th:class):

<tr th:class="${row.even}? 'even' : 'odd'">
  ...
</tr>

Все три части условного выражения (condition, then и else) сами являются выражениями, что означает, что они могут быть переменными (${...}, *{...}), messages (#{...}), URLs (@{...}) или литералами ('...').

Условные выражения также могут быть вложены с помощью круглых скобок:

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
  ...
</tr>

Другие выражения также могут быть опущены, и в этом случае возвращается null значение, если условие ложно:

<tr th:class="${row.even}? 'alt'">
  ...
</tr>

4.12 По умолчанию/Default выражение (Elvis оператор)


Выражение по умолчанию — это особый вид условного значения без какой-либо части. Это эквивалентно оператору Elvis, присутствующему на некоторых языках, например Groovy, что позволяет указать два выражения: первый используется, если он не null, но если это так, то используется второй.

Посмотрим, как это работает на странице нашего профиля:

<div th:object="${session.user}">
  ...
  <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>

Как вы можете видеть, оператором является ?:, И мы используем его здесь, чтобы указать значение по умолчанию для имени (буквальное значение в этом случае), только если результат вычисления *{age} равен null. Поэтому это эквивалентно:

<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>

Как и условные значения, они могут содержать вложенные выражения между круглыми скобками:

<p>
  Name: 
  <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span>
</p>

4.13 No-Operation токен



Токен No-Operation представлен символом (_).

Идея этого токена состоит в том, чтобы указать, что желаемый результат для выражения — ничего не делать: делать точно так, как если бы обработанный атрибут (например, th:text) вообще отсутствовал.

Среди других возможностей это позволяет разработчикам использовать текст прототипов в качестве значений по умолчанию. Например, вместо:

<span th:text="${user.name} ?: 'no user authenticated'">...</span>

… мы можем напрямую использовать ‘no user authenticated’ в качестве текста прототипирования, что приводит к тому, что код является более кратким и универсальным с точки зрения дизайна:

<span th:text="${user.name} ?: _">no user authenticated</span>

4.15 Преобразование данных / Форматирование


Thymeleaf определяет синтаксис двойных скобок для переменных (${...}) и выделенных (*{...}) выражений, что позволяет нам применя преобразование данных с использование конфигурирования сервиса преобразований.

Базово это выглядит так:

<td th:text="${{user.lastAccessDate}}">...</td>

Вы заметили двойную скобку?: ${{...}}. Это инструктирует Thymeleaf передать результат выражения user.lastAccessDate в службу преобразования и попросит выполнить операцию форматирования (преобразование в String) перед возвращением результата.

Предполагая, что user.lastAccessDate имеет тип java.util.Calendar, если служба преобразования (реализация IStandardConversionService) была зарегистрирована и содержит действительное преобразование для Calendar -> String, оно будет применяться.

По умолчанию реализация IStandardConversionService (класс StandardConversionService) просто выполняет .toString() для любого объекта. Дополнительные сведения о том, как зарегистрировать пользовательскую реализацию службы преобразования, можно найти в разделе «Дополнительные сведения о конфигурации».

Официальные пакеты интеграции thymeleaf-spring3 и thymeleaf-spring4 прозрачно интегрируют механизм обслуживания преобразования Thymeleaf с собственной инфраструктурой Conversion Service Spring, так что сервисы конвертации и форматы, объявленные в конфигурации Spring, будут автоматически доступны для выражений ${{...}} и *{{...}}.

4.14 Препроцессинг


В дополнение ко всем этим функциям для обработки выражений Thymeleaf имеет функцию препроцессорных выражений.

Предварительная обработка — это выполнение выражений, сделанных до выполнения стандартных выражений, что позволяет модифицировать выражение, которое в конечном итоге будет выполнено.

Предварительно обработанные выражения точно аналогичны нормальным, но появляются в окружении двойного символа подчеркивания (например, __${выражение}__).

Предположим, у нас есть запись i18n Messages_fr.properties, содержащая выражение OGNL, вызывающее статический метод, специфичный для языка, например:

article.text=@myapp.translator.Translator@translateToFrench({0})

… и эквивалент Messages_es.properties:

article.text=@myapp.translator.Translator@translateToSpanish({0})

Мы можем создать фрагмент разметки, который выполняет одно выражение или другое в зависимости от локали. Для этого мы сначала выберем выражение (путем предварительной обработки), а затем запустим Thymeleaf:

<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>

Обратите внимание, что шаг предварительной обработки для французского языка будет создавать следующий эквивалент:

<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}">Some text here...</p>

Строка препроцессора __ может быть экранирована в атрибутах с помощью \_\_.

Комментарии (2)


  1. Vest
    14.03.2018 15:14

    Скажите, пожалуйста, насколько это оправдано мешать логику с View с помощью подобных выражений?

    'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

    Есть ли какие-либо рекомендации со стороны Thymeleaf, например, создавать функции-хелперы (или форматтеры), которые бы вызывались из View и ничего больше?

    Спасибо.


    1. pilot911 Автор
      15.03.2018 09:04

      возможно, дальше по учебнику будут примеры, я пока только осваиваю Thymeleaf и делаю перевод для удобства всех