Skip to content

StringTemplate

Sayapin Alexander edited this page Sep 17, 2015 · 6 revisions

String template (http://www.stringtemplate.org/) (далее ST) - это библиотека для обработки шаблонов. ST является подпроектом в составе Antlr (http://www.antlr.org/). Собственно Antlr использует ST для генерации кода. ST был специально реализован для обработки шаблонов и генерации кода.

Общие сведения

Создадим простой проект для иллюстрации работы ST.

Добавим зависимость ST в pom.xml.

<dependencies>
    <dependency>
        <groupId>org.antlr</groupId>
        <artifactId>ST4</artifactId>
        <version>4.0.8</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

Добавим класс с методом main.

package stringtemplate.learn;

import org.stringtemplate.v4.ST;

public class Main {
    public static void main(String[] args) {
        ST st = new ST("Hello, <place>!");
        
        st.add("place", "world");
        
        System.out.println(st.render());
    }
}

Выполним приложение.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
Hello, world!

Как видим атрибут был заменён на строку "world".

Задание шаблонов с параметрами

ST отличается от других шаблонизаторов тем, что позволяет описывать шаблоны с параметрами.

Например, создадим шаблон для вывода кода объявление поля в Java. В качестве параметров будет передавать имя и тип поля.

Шаблон будет иметь следующий вид:

fieldDeclaration(type, name) ::= <<
	protected <type> <name>;
>>

Таким образом, был объявлен шаблон с именем fieldDeclaration с 2-мя параметрами:

  • type
  • name

Описание шаблона имеет следующий вид:

ИМЯ_ШАБЛОНА(ПАРАМЕТРЫ) ::= <<
	МНОГОСТРОЧНЫЙ_ШАБЛОН
>>

Так как мы создаём не "плоский" шаблон, а набор шаблонов, то нам необходимо внести ряд изменений для поддержки групп шаблонов.

Для начала объявим объект типа STGroupTemplate, который поддерживает описание групп шаблонов. Получение объекта шаблона осуществляется с помощью метода getInstanceOf куда передаётся название шаблона.

Результирующий код будет выглядеть так:

package stringtemplate.learn;

import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupString;

public class Main {
    public static void main(String[] args) {
        STGroupString st = 
            new STGroupString(
                "fieldDeclaration(type, name) ::= <<\n" +
                "        protected <type> <name>;\n" +
                ">>"
            );
        
        ST tpl = st.getInstanceOf("fieldDeclaration");
        
        tpl.add("type", "DefaultSmppSession");
        tpl.add("name", "smppSession");
        
        System.out.println(tpl.render());
    }
}

Запустим данный код и посмотрим на вывод.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
protected DefaultSmppSession smppSession;

Как видно изменения были применены к нашим параметрам и на выходе была получена ожидаемая строка.

Работа с подшаблонами

Как видно из предыдущего примера, шаблоны имеют имена и что более важно, могут быть использованы в тексте другого шаблона.

Например, мы можем реализовать вывод методов get* и set* для создаваемого поля.

Объявим соответствующие шаблоны:

setter(type, name) ::= <<
public void set<name>(<type> <name>) {
        this.<name> = <name>;
}
>>

getter(type, name) ::= <<
public <type> get<name>() {
        return this.<name>;
}
>>

Код же изменился только в части описания шаблона.

package stringtemplate.learn;

import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupString;

public class Main {
    public static void main(String[] args) {
        STGroupString st = 
            new STGroupString(
                "fieldDeclaration(type, name) ::= <<\n" +
                "        protected <type> <name>;\n" +
                "        <setter(type, name)>\n" +
                "        <getter(type, name)>\n" +
                ">>" +
                "setter(type, name) ::= <<\n" +
                "public void set<name>(<type> <name>) {\n" +
                "        this.<name> = <name>;\n" +
                "}\n" +
                ">>\n" +
                "\n" +
                "getter(type, name) ::= <<\n" +
                "public <type> get<name>() {\n" +
                "        return this.<name>;\n" +
                "}\n" +
                ">>"
            );
        
        ST tpl = st.getInstanceOf("fieldDeclaration");
        
        tpl.add("type", "DefaultSmppSession");
        tpl.add("name", "smppSession");
        
        System.out.println(tpl.render());
    }
}

Запустим приложение и посмотрим на вывод.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
        protected DefaultSmppSession smppSession;
        public void setsmppSession(DefaultSmppSession smppSession) {
                this.smppSession = smppSession;
        }
        public DefaultSmppSession getsmppSession() {
                return this.smppSession;
        }

Как видим были созданы соответствующие методы.

Добавление формата строк

ST осуществляет не простой вывод в поток, а применяет некоторые преобразования. Кроме того, при рендеринге значений возможна передача параметров в объект рендерера, который осуществляет вывод.

Например, подключим StringRenderer.

import org.stringtemplate.v4.StringRenderer;
...
st.registerRenderer(String.class, new StringRenderer());

StringRenderer - это класс, который отвечает за рендеринг строк. Ему передаётся в частности параметр format, который изменяет строку. Возможные форматы:

  • upper - привести к верхнему регистру
  • lower - привести к нижнему регистру
  • cap - сделать первую букву заглавной
  • url-encode - провести URL-кодирование

Передача параметров осуществляется посредством задания параметра и значения, после точки с запятой (;) после имени атрибута.

Пример.

<name;format="cap">

Данный механизм позволит нам реализовать преобразования для задания правильных имён для геттеров и сеттеров.

Изменим шаблоны следующим образом:

setter(type, name) ::= <<
public void set<name; format="cap">(<type> <name>) {
        this.<name> = <name>;
}
>>

getter(type, name) ::= <<
public <type> get<name; format="cap">() {
        return this.<name>;
}
>>

Как видим добавилось указание параметра формат для формирования имени геттеров и сеттеров.

Полный код класса.

package stringtemplate.learn;

import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupString;
import org.stringtemplate.v4.StringRenderer;

public class Main {
    public static void main(String[] args) {
        STGroupString st = 
            new STGroupString(
                "fieldDeclaration(type, name) ::= <<\n" +
                "        protected <type> <name>;\n" +
                "        <setter(type, name)>\n" +
                "        <getter(type, name)>\n" +
                ">>" +
                "setter(type, name) ::= <<\n" +
                "public void set<name; format=\"cap\">(<type> <name>) {\n" +
                "        this.<name> = <name>;\n" +
                "}\n" +
                ">>\n" +
                "\n" +
                "getter(type, name) ::= <<\n" +
                "public <type> get<name; format=\"cap\">() {\n" +
                "        return this.<name>;\n" +
                "}\n" +
                ">>"
            );
        
        st.registerRenderer(String.class, new StringRenderer());
        
        ST tpl = st.getInstanceOf("fieldDeclaration");
        
        tpl.add("type", "DefaultSmppSession");
        tpl.add("name", "smppSession");
        
        System.out.println(tpl.render());
    }
}

Запустим и посмотрим на вывод.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
        protected DefaultSmppSession smppSession;
        public void setSmppSession(DefaultSmppSession smppSession) {
                this.smppSession = smppSession;
        }
        public DefaultSmppSession getSmppSession() {
                return this.smppSession;
        }

Теперь названия геттеров и сеттеров соответствуют правилам именования для java beans.

Подгрузка шаблонов из фалов

ST поддерживает возможность получения шаблонов из файлов, а не в виде строки, как было показано ранее.

Создадим файл tpl.stg в директории src/main/resources следующего содержания

fieldDeclaration(type, name) ::= <<
        protected <type> <name>;
        <setter(type, name)>
        <getter(type, name)>
>>

setter(type, name) ::= <<
public void set<name; format="cap">(<type> <name>) {
        this.<name> = <name>;
}
>>

getter(type, name) ::= <<
public <type> get<name; format="cap">() {
        return this.<name>;
}
>>

Изменим код основного класса на загрузку из файла.

package stringtemplate.learn;

import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupFile;
import org.stringtemplate.v4.StringRenderer;

public class Main {
    public static void main(String[] args) {
        STGroupFile st = new STGroupFile("src/main/resources/tpl.stg");
        
        st.registerRenderer(String.class, new StringRenderer());
        
        ST tpl = st.getInstanceOf("fieldDeclaration");
        
        tpl.add("type", "DefaultSmppSession");
        tpl.add("name", "smppSession");
        
        System.out.println(tpl.render());
    }
}

Запустим проект для проверки.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
        protected DefaultSmppSession smppSession;
        public void setSmppSession(DefaultSmppSession smppSession) {
                this.smppSession = smppSession;
        }
        public DefaultSmppSession getSmppSession() {
                return this.smppSession;
        }

Как видно вывод остался прежним, хотя шаблоны берутся не из строки, а из файла с шаблонами.

Работа с объектами и списками

ST умеет извлекать атрибуты из объектов. Например, чтобы обратиться к атрибуту atr в объекте obj необходимо написать.

<obj.atr>

Кроме того, ST позволяет применять шаблоны итеративно с помощью следующего синтаксиса:

<АТРИБУТ_СО_СПИСКОМ:{ НАЗВАНИЕ_НОВОГО_АТРИБУТА | ПРИМЕНЯЕМЫЙ_ШАБЛОН }>

Изменим наш шаблон таким образом, чтобы генерировать класс с полями и методами.

Файл шаблонов tpl.stg

classDefinition(name, fields) ::=<<
public class <name> {
    <fields:{field | <fieldDeclaration(field.type, field.name)>}>

    <fields:{field | <setter(field.type, field.name)>}>
    <fields:{field | <getter(field.type, field.name)>}>
}
>>

fieldDeclaration(type, name) ::= <<
protected <type> <name>;

>>

setter(type, name) ::= <<
public void set<name; format="cap">(<type> <name>) {
        this.<name> = <name>;
}

>>

getter(type, name) ::= <<
public <type> get<name; format="cap">() {
        return this.<name>;
}

>>

Перевод строки (и пустая строка) обязательны, чтобы выводы располагались на разных строках.

Изменим основной класс.

package stringtemplate.learn;

import java.util.ArrayList;
import java.util.List;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupFile;
import org.stringtemplate.v4.StringRenderer;

public class Main {
    public static void main(String[] args) {
        STGroupFile st = new STGroupFile("src/main/resources/tpl.stg");
        
        st.registerRenderer(String.class, new StringRenderer());
        
        ST tpl = st.getInstanceOf("classDefinition");
        
        List<Field> fields = new ArrayList<>();
        
        fields.add(new Field("connection", "SmppServerConnection"));
        fields.add(new Field("size", "long"));
        fields.add(new Field("timeout", "long"));
        
        tpl.add("fields", fields);
        tpl.add("name", "ClientConnection");
        
        System.out.println(tpl.render());
    }
    
    public static class Field {
        protected String name;
        protected String type;

        public Field() {
        }
        
        public Field(String name, String type) {
            this.name = name;
            this.type = type;
        }
        
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }        
    }
}

В данном классе создаётся список описаний полей и передаётся в шаблон, а также название класса. На выходе мы должны получить описание класса с полями, геттерами и сеттерами.

Запустим изменённое приложение.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
public class ClientConnection {
    protected SmppServerConnection connection;
    protected long size;
    protected long timeout;


    public void setConnection(SmppServerConnection connection) {
            this.connection = connection;
    }
    public void setSize(long size) {
            this.size = size;
    }
    public void setTimeout(long timeout) {
            this.timeout = timeout;
    }

    public SmppServerConnection getConnection() {
            return this.connection;
    }
    public long getSize() {
            return this.size;
    }
    public long getTimeout() {
            return this.timeout;
    }

}

То есть на выходе получаем полностью созданный класс.

Параметры поумолчанию

ST позволяет объявлять параметры поумолчанию для шаблонов путём указания значения для формального параметра.

Пример,

template(param1, param2 = "someValue") ::= <<
	param1 = param2;
>>

В нашем случае добавим указание квалификатора доступа в качестве параметра поумолчанию.

fieldDeclaration(type, name, qualifier="protected") ::= <<
<qualifier> <type> <name>;

>>

Условные конструкции

ST позволяет использовать в коде условные конструкции. Добавим к шаблону группировку полей по типу квалификаторов доступа.

Для начала расширим класс поля (Field), добавив поле qualifier. Кроме того, условная логика в ST требует, чтобы выражения в условных блоках были булевыми и состояли только из обращений к атрибутам и методам (это необходимо для соблюдения принципа разделения обработки и отображения), поэтому нам потребутеся реализовать 3 метода (isPublic, isProtected, isPrivate).

public static class Field {
        protected String name;
        protected String type;
        protected String qualifier = "protected";
        
        public Field() {
        }
        
        public Field(String name, String type) {
            this.name = name;
            this.type = type;
        }

        public Field(String name, String type, String qualifier) {
            this.name = name;
            this.type = type;
            this.qualifier = qualifier;
        }

        public boolean isPublic() {
            return "public".equals(getQualifier());
        }
        
        public boolean isProtected() {
            return "protected".equals(getQualifier());
        }
        
        public boolean isPrivate() {
            return "private".equals(getQualifier());
        }
	...
}

Сделаем поле size публичным.

fields.add(new Field("connection", "SmppServerConnection"));
fields.add(new Field("size", "long", "public"));
fields.add(new Field("timeout", "long"));

Добавим условные рендеринг в шаблон.

Пример.

<fields:{field | <if(field.public)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>

Условная конструкция заключена между тэгами <if()> и <endif>.

Полный код шаблона.

classDefinition(name, fields) ::=<<
public class <name> {
    <fields:{field | <if(field.public)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>
    <fields:{field | <if(field.protected)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>
    <fields:{field | <if(field.private)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>

    <fields:{field | <setter(field.type, field.name)>}>
    <fields:{field | <getter(field.type, field.name)>}>
}
>>

fieldDeclaration(type, name, qualifier="protected") ::= <<
<qualifier> <type> <name>;

>>

setter(type, name) ::= <<
public void set<name; format="cap">(<type> <name>) {
        this.<name> = <name>;
}

>>

getter(type, name) ::= <<
public <type> get<name; format="cap">() {
        return this.<name>;
}

>>

Запустим приложение.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
public class ClientConnection {
    public long size;

    protected SmppServerConnection connection;
    protected long timeout;


    public void setConnection(SmppServerConnection connection) {
            this.connection = connection;
    }
    public void setSize(long size) {
            this.size = size;
    }
    public void setTimeout(long timeout) {
            this.timeout = timeout;
    }

    public SmppServerConnection getConnection() {
            return this.connection;
    }
    public long getSize() {
            return this.size;
    }
    public long getTimeout() {
            return this.timeout;
    }

}

Как видно поля отсортированы по типу квалификаторов доступа.

Вывод массивов

ST позволяет выводить массивы значений с указанием раздлителя.

Например, добавим в класс набор целоцисленных констант.

Шаблон tpl.stg.

classDefinition(name, fields, values) ::=<<
...
    public static int[] CONSTANTS = new int[]{<values; separator=", ">};

Добавим изменения в класс Main.

tpl
    .add(
        "values",
        new int[] {
            1,5,8,7,4,5,8,7,5,6,9,5,4,1,
            2,5,4,7,8,5,4,1,2,54,21,5,46,
            4,64,6,4,6,87,3,2,1,1,4,5,7,23,
            4,4,6,7,8,4,2,2,5,6,8,9,0,6,7,2,4,
            5,5,8,5,2,6,5,54,4,8,61,9,8,54,5,5,
            49,1
        }
    );

Вывод в данном случае будет не очень красивым.

...
    public static int[] CONSTANTS = new int[]{ 1, 5, 8, 7, 4, 5, 8, 7, 5, 6, 9, 5, 4, 1, 2, 5, 4, 7, 8, 5, 4, 1, 2, 54, 21, 5, 46, 4, 64, 6, 4, 6, 87, 3, 2, 1, 1, 4, 5, 7, 23, 4, 4, 6, 7, 8, 4, 2, 2, 5, 6, 8, 9, 0, 6, 7, 2, 4, 5, 5, 8, 5, 2, 6, 5, 54, 4, 8, 61, 9, 8, 54, 5, 5, 49, 1 };
...

Все значения выведены в одну строку.

Допустим мы генерируем файл с максимальной длиной строки в 80 символов, тогда необходимо указать данную настройку при рендеринге.

System.out.println(tpl.render(80));

Если указать длину строки, то можно воспользоваться опцией wrap, которая позволяет переносить текст на новую строку.

...
public static int[] CONSTANTS = new int[]{ <values; wrap, separator=", " > };
...

Вывод будет выглядеть так:

...
    public static int[] CONSTANTS = new int[]{ 1, 5, 8, 7, 4, 5, 8, 7, 5, 6, 9, 
5, 4, 1, 2, 5, 4, 7, 8, 5, 4, 1, 2, 54, 21, 5, 46, 4, 64, 6, 4, 6, 87, 3, 2, 1, 
1, 4, 5, 7, 23, 4, 4, 6, 7, 8, 4, 2, 2, 5, 6, 8, 9, 0, 6, 7, 2, 4, 5, 5, 8, 5, 2, 
6, 5, 54, 4, 8, 61, 9, 8, 54, 5, 5, 49, 1 };
...

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

...
public static int[] CONSTANTS = new int[]{ 
                                    <values; wrap, anchor, separator=", " > 
                                };
...

Вывод в данном случае будет таким:

...
    public static int[] CONSTANTS = new int[]{ 
                                        1, 5, 8, 7, 4, 5, 8, 7, 5, 6, 9, 5, 4, 1, 
                                        2, 5, 4, 7, 8, 5, 4, 1, 2, 54, 21, 5, 46, 
                                        4, 64, 6, 4, 6, 87, 3, 2, 1, 1, 4, 5, 7, 
                                        23, 4, 4, 6, 7, 8, 4, 2, 2, 5, 6, 8, 9, 
                                        0, 6, 7, 2, 4, 5, 5, 8, 5, 2, 6, 5, 54, 
                                        4, 8, 61, 9, 8, 54, 5, 5, 49, 1 
                                    };
...

Полный код шаблона:

classDefinition(name, fields, values) ::=<<
public class <name> {
    public static int[] CONSTANTS = new int[]{ 
                                        <values; wrap, anchor, separator=", " > 
                                    };

    <fields:{field | <if(field.public)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>
    <fields:{field | <if(field.protected)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>
    <fields:{field | <if(field.private)><fieldDeclaration(field.type, field.name, field.qualifier)><endif>}>

    <fields:{field | <setter(field.type, field.name)>}>
    <fields:{field | <getter(field.type, field.name)>}>
}
>>

fieldDeclaration(type, name, qualifier="protected") ::= <<
<qualifier> <type> <name>;

>>

setter(type, name) ::= <<
public void set<name; format="cap">(<type> <name>) {
        this.<name> = <name>;
}

>>

getter(type, name) ::= <<
public <type> get<name; format="cap">() {
        return this.<name>;
}

>>

Полный код класса (Main.java):

package stringtemplate.learn;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.antlr.runtime.Token;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroupFile;
import org.stringtemplate.v4.StringRenderer;

public class Main {
    public static void main(String[] args) {
        STGroupFile st = new STGroupFile("src/main/resources/tpl.stg");
        
        st.registerRenderer(String.class, new StringRenderer());
        
        ST tpl = st.getInstanceOf("classDefinition");
        
        List<Field> fields = new ArrayList<>();
        
        fields.add(new Field("connection", "SmppServerConnection"));
        fields.add(new Field("size", "long", "public"));
        fields.add(new Field("timeout", "long"));
        
        tpl.add("fields", fields);
        tpl.add("name", "ClientConnection");
        tpl
            .add(
                "values",
                new int[] {
                    1,5,8,7,4,5,8,7,5,6,9,5,4,1,
                    2,5,4,7,8,5,4,1,2,54,21,5,46,
                    4,64,6,4,6,87,3,2,1,1,4,5,7,23,
                    4,4,6,7,8,4,2,2,5,6,8,9,0,6,7,2,4,
                    5,5,8,5,2,6,5,54,4,8,61,9,8,54,5,5,
                    49,1
                }
            );
        
        System.out.println(tpl.render(80));
    }
    
    public static class Field {
        protected String name;
        protected String type;
        protected String qualifier = "protected";
        
        public Field() {
        }
        
        public Field(String name, String type) {
            this.name = name;
            this.type = type;
        }

        public Field(String name, String type, String qualifier) {
            this.name = name;
            this.type = type;
            this.qualifier = qualifier;
        }

        public boolean isPublic() {
            return "public".equals(getQualifier());
        }
        
        public boolean isProtected() {
            return "protected".equals(getQualifier());
        }
        
        public boolean isPrivate() {
            return "private".equals(getQualifier());
        }
        
        public String getQualifier() {
            return qualifier;
        }

        public void setQualifier(String qualifier) {
            this.qualifier = qualifier;
        }
        
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }
    }
}

Вывод приложения.

$ mvn clean compile exec:java -D'exec.mainClass=stringtemplate.learn.Main'
...
public class ClientConnection {
    public static int[] CONSTANTS = new int[]{ 
                                        1, 5, 8, 7, 4, 5, 8, 7, 5, 6, 9, 5, 4, 1, 
                                        2, 5, 4, 7, 8, 5, 4, 1, 2, 54, 21, 5, 46, 
                                        4, 64, 6, 4, 6, 87, 3, 2, 1, 1, 4, 5, 7, 
                                        23, 4, 4, 6, 7, 8, 4, 2, 2, 5, 6, 8, 9, 
                                        0, 6, 7, 2, 4, 5, 5, 8, 5, 2, 6, 5, 54, 
                                        4, 8, 61, 9, 8, 54, 5, 5, 49, 1 
                                    };

    public long size;

    protected SmppServerConnection connection;
    protected long timeout;


    public void setConnection(SmppServerConnection connection) {
            this.connection = connection;
    }
    public void setSize(long size) {
            this.size = size;
    }
    public void setTimeout(long timeout) {
            this.timeout = timeout;
    }

    public SmppServerConnection getConnection() {
            return this.connection;
    }
    public long getSize() {
            return this.size;
    }
    public long getTimeout() {
            return this.timeout;
    }
}
Clone this wiki locally