FreeMarker шаблоны

в 4:27, , рубрики: freemarker, html, java

Apache FreeMarker — это механизм шаблонов: библиотека Java для генерации текстового вывода (HTML-страницы, xml, файлы конфигурации, исходный код и.т.д. На вход подается шаблон, например html в котором есть специальные выражения, подготавливаются данные соответствующие этим выражением, а Freemarker динамически вставляет эти данные и получается динамически заполненный документ.
image

В статье FreeMarker
Spring boot
Macros
REST API

Т.е. простое выражение на freemarker это например ${name}, в выражения поддерживаются вычисления, операции сравнения, условия, циклы, списки, встроенные функции, макрос и много др. Пример html с выражением ${name} (шаблон test.ftl)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>${name}!</title>
</head>
<body>
   <h2>Hello ${name}!</h2>    
</body>
</html>

Если теперь создать в java модель данных

import freemarker.template.Configuration;
import freemarker.template.Template;
...
// Конфигурация
Configuration cfg = new Configuration(Configuration.VERSION_2_3_27);
// модель данных
Map<String, Object> root = new HashMap<>();
root.put("name", "Freemarker");
// шаблон
Template temp = cfg.getTemplate("test.ftl");
// обработка шаблона и модели данных
Writer out = new OutputStreamWriter(System.out);
// вывод в консоль
temp.process(root, out);

то получим html документ с заполненным name.
Если надо обработать список, то используется конструкция #list, например для html списка

<ul>
  <#list father as item>
      <li>${item}</li>
  </#list>
</ul>

В java, в модель данных подать список можно так
Map<String, Object> root = new HashMap<>();
....
root.put("father", Arrays.asList("Alexander", "Petrov", 47));

Перейдем к Spring

В Spring boot есть поддержка Freemarker. На сайте SPRING INITIALIZR можно получить pom файл проекта.

pom файл

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>demoFreeMarker</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>demoFreeMarker</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-freemarker</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

класс DemoFreeMarkerApplication
@SpringBootApplication
public class DemoFreeMarkerApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoFreeMarkerApplication.class, args);
	}
}

В Spring есть уже подготовленный компонент конфигурации Configuration для freemarker.
Для примера консольного приложения возьму spring интерфейс для обработки командной строки(CommandLineRunner) и подготовлю модель данных для следующего шаблона ftl (hello_test.ftl)

Шаблон hello_test.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello ${name}!</title>
</head>
<body>

<input type="text" placeholder="${name}">

<table>
    <#list persons as row>
    <tr>
        <#list row as field>
            <td>${field}</td>
        </#list>
    </tr>
    </#list>
</table>

</body>
</html>

Java код для модели данных шаблона hello_test.ftl

класс CommandLine и модель данных

@Component
public class CommandLine implements CommandLineRunner {

    @Autowired
    private Configuration configuration;

    public void run(String... args) {
        Map<String, Object> root = new HashMap<>();
        // для ${name}
        root.put("name", "Fremarker");
        // для <#list persons
        List<List> persons = new ArrayList<>();
        persons.add(Arrays.asList("Alexander", "Petrov", 47));
        persons.add(Arrays.asList("Slava", "Petrov", 13));
        root.put("persons", persons);

        try {
            Template template = configuration.getTemplate("hello_test.ftl");
            Writer out = new OutputStreamWriter(System.out);
            try {
                template.process(root, out);
            } catch (TemplateException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

После обработки получим html документ

Output html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello Fremarker!</title>
</head>
<body>

<input type="text" placeholder="Fremarker">

<table>
    <tr>
            <td>Alexander</td>
            <td>Petrov</td>
            <td>47</td>
    </tr>
    <tr>
            <td>Slava</td>
            <td>Petrov</td>
            <td>13</td>
    </tr>
</table>
</body>

Макросы

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

<#macro textInput id value="">
  <input type="text" id="${id}" value="${value}">
</#macro>

Это макрос с именем textInput и параметрами id (он обязательный) и value (он не обязательный, т.к. имеет значение по умолчанию). Далее идет его тело и использование входных параметров. В шаблоне файл с макросами подключается так:

<#import "ui.ftl" as ui/>

Из шаблона макрос вызывается так:

<@ui.textInput id="name" value="${name}"/>

Где ui это алиас который указали при подключении, ${name} переменная в модели, далее через алиас ссылаемся на имя макроса textInput и указываем его параметры, как минимум обязательные. Подготовлю простые макросы для html Input и Table

файл макросов ui.ftl

<#-- textInput macro for html input -->
<#macro textInput id placeholder="" value="">
  <input type="text" id="${id}" placeholder="${placeholder}" value="${value}">
</#macro>

<#-- table macro for html table -->
<#macro table id rows>
<table id="${id}">
    <#list rows as row>
    <tr>
        <td>${row?index + 1}</td>
        <#list row as field>
            <td>${field}</td>
        </#list>
    </tr>
    </#list>
</table>
</#macro>

${row?index + 1} это встроенная поддержка индекса элемента списка, подобных встроенных функций много. Если теперь изменить предыдущий основной шаблон и заменить в нем input и table на макросы, то получится такой документ

Шаблон hello.ftl

<#import "ui.ftl" as ui/>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello ${name}!</title>
</head>
<body>

<@ui.textInput id="name" placeholder="Enter name" value="${name}"/>
<@ui.table id="table1" rows=persons/>

</body>
</html>

REST

Конечно такую модель удобно использовать в web приложении. Подключаю зависимость в pom

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

Добавляю REST Controller

DemoController.java

@Controller
public class DemoController {

    @Autowired
    private RepositoryService repositoryService;

    @GetMapping("/")
    public String index() {
        return "persons";
    }

    @RequestMapping(value = "/search", method = RequestMethod.POST)
    public String hello(Model model, @RequestParam(defaultValue = "") String searchName) {
        List<List<String>> persons = repositoryService.getRepository();
        List<List<String>> filterList = persons.stream()
                .filter(p -> p.get(0).contains(searchName))
                .collect(Collectors.toList());
        model.addAttribute("persons", filterList);
        model.addAttribute("lastSearch", searchName);
        return "persons";
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public String save(Model model, @ModelAttribute("person") Person person) {
        List<List<String>> persons = repositoryService.addPerson(person);
        model.addAttribute("persons", persons);
        return "persons";
    }
}

Service репозиторий для лиц

RepositoryService.java

@Service
public class RepositoryService {

    private static List<List<String>> repository = new ArrayList<>();

    public List<List<String>> getRepository() {
        return repository;
    }

    public List<List<String>> addPerson(Person person) {
        repository.add(Arrays.asList(person.getFirstName(), person.getAge().toString()));
        return repository;
    }
}

Класс лицо

Person.java

public class Person {

    public Person(String firstName, Integer age) {
        this.firstName = firstName;
        this.age = age;
    }

    private String firstName;
    private Integer age;

    public String getFirstName() {
        return firstName;
    }

    public Integer getAge() {
        return age;
    }
}

Шаблон макросов

ui.ftl

<#macro formInput id name label type="text" value="">
<label for="${id}">${label}</label>
<input type="${type}" id="${id}" name="${name}" value="${value}">
</#macro>

<#macro table id rows>
<table id="${id}" border="1px" cellspacing="2" border="1" cellpadding="5">
    <#list rows as row>
        <tr>
            <td>${row?index + 1}</td>
            <#list row as field>
                <td>${field}</td>
            </#list>
        </tr>
    </#list>
</table>
</#macro>

Основной шаблон

persons.ftl

<#import "ui.ftl" as ui/>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Person</title>
    <link href="style/my.css" rel="stylesheet">
</head>
<body>

<div>
    <fieldset>
        <legend>Добавить лицо</legend>
        <form name="person" action="save" method="POST">
            <@ui.formInput id="t1" name="firstName" label="Имя"/> <br/>
            <@ui.formInput id="t2" name="age" label="Возраст"/> <br/>
            <input type="submit" value="Save" />
        </form>
    </fieldset>
</div>

<div>
    <fieldset>
        <legend>Поиск</legend>
        <form name="searchForm" action="search" method="POST">
        <@ui.formInput id="t3" name="searchName" label="Поиск"/> <br/>
            <input type="submit" value="Search" />
        </form>
    </fieldset>
</div>
<p><#if lastSearch??>Поиск для: ${lastSearch}<#else></#if></p>

<@ui.table id="table1" rows=persons![]/>

</body>
</html>

Структура проекта
image

Приложение будет обрабатывать две команды «save» и «search» лица (см. контроллер). Всю работу по обработке (мапингу) входных параметров, берет на себя Spring.

Некоторые пояснения к шаблону.
<#if lastSearch??>Поиск для: ${lastSearch}<#else></#if>
здесь проверяется, если параметр задан, то вывести фразу «Поиск для: ..», иначе ничего
<@ui.table id="table1" rows=persons![]/>
здесь тоже сделана проверка, что список лиц присутствует, иначе пустой. Эти проверки важны при первом открытии страницы, иначе пришлось бы их инициализировать в index(), контроллера.

Работа приложения
image

Материалы:
Apache FreeMarker Manual

Автор: Александр

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js