пятница, 9 мая 2008 г.

Развивая тему Guice: прикручиваем Velocity с помощью Guice.


Данный пост написан не с целью развития холивара на тему, что лучше Velocity или JSP, но здесь я хочу продемонстрировать как можно подключать такой удобный шаблонный движок, как Velocity к приложению, построенному на базе Guice.

О том, что такое Velocity и многих аспектах его использования можно почитать на форуме программистов и конечно же официальной странице проекта.

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

package ru.sposition.web.view;



import java.io.Writer;

import java.util.Map;



import ru.sposition.web.view.exception.TemplateEngineException;



public interface ITemplateEngine {

   

    public void mergeTemplate(String templateLocation, Map<String, Object> model,

            Writer writer) throws TemplateEngineException;

   

    public void mergeTemplate(String templateLocation, String encoding,

            Map<String, Object> model, Writer writer) throws TemplateEngineException;

   

    public String mergeTemplateIntoString(String templateLocation,

            Map<String, Object> model) throws TemplateEngineException;

   

    public String mergeTemplateIntoString(String templateLocation, String encoding,

            Map<String, Object> model) throws TemplateEngineException;

}

 


Все взаимодействие приложения с механизмом шаблонов теперь достаточно строить через данный интерфейс, он предоставляет все необходимые для этого методы. Для того, чтобы использовать в качестве шаблонного движка Velocity необходимо реализовать данный интерфейс с использованием VelocityEngine. Собственно данный механизм реализован в проекте Spring Framework (класс VelocityEngineUtils), код данной реализации и возьмем за основу, слегка его модифицировав:

package ru.sposition.web.view.velocity;



import java.io.StringWriter;

import java.io.Writer;

import java.util.Map;



import org.apache.velocity.VelocityContext;

import org.apache.velocity.app.VelocityEngine;



import ru.sposition.web.view.ITemplateEngine;

import ru.sposition.web.view.exception.TemplateEngineException;

import ru.sposition.web.view.velocity.annotations.Velocity;



import com.google.inject.Inject;



/**

 * Utility class for working with a VelocityEngine. Provides convenience methods

 * to merge a Velocity template with a model.

 *

 * @author Juergen Hoeller

 * @author Samolisov Pavel <samolisov@gmail.com>

 */


public class VelocityTemplateEngine implements ITemplateEngine {

   

    private VelocityEngine velocityEngine;

       

    @Inject

    public void injectVelocitiEngine(@Velocity VelocityEngine engine) {

        this.velocityEngine = engine;

    }

   

    /**

     * Merge the specified Velocity template with the given model and write the

     * result to the given Writer.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param model the Map that contains model names as keys and model objects as values

     * @param writer the Writer to write the result to

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public void mergeTemplate(String templateLocation, Map<String, Object> model,

            Writer writer) throws TemplateEngineException {

        try {

            VelocityContext velocityContext = new VelocityContext(model);

            velocityEngine.mergeTemplate(templateLocation, velocityContext, writer);       

        } catch (RuntimeException ex) {

            throw ex;

        } catch (Exception ex) {

            throw new TemplateEngineException(ex.getMessage());

        }

    }



    /**

     * Merge the specified Velocity template with the given model and write the

     * result to the given Writer.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param encoding the encoding of the template file

     * @param model the Map that contains model names as keys and model objects as values

     * @param writer the Writer to write the result to

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public void mergeTemplate(String templateLocation, String encoding,

            Map<String, Object> model, Writer writer) throws TemplateEngineException {

        try {

            VelocityContext velocityContext = new VelocityContext(model);

            velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);     

        } catch (RuntimeException ex) {

            throw ex;

        } catch (Exception ex) {

            throw new TemplateEngineException(ex.getMessage());

        }

    }



    /**

     * Merge the specified Velocity template with the given model into a String.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param model the Map that contains model names as keys and model objects as values

     * @return the result as String

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public String mergeTemplateIntoString(String templateLocation,

            Map<String, Object> model) throws TemplateEngineException {

        StringWriter result = new StringWriter();

        mergeTemplate(templateLocation, model, result);

        return result.toString();

    }



    /**

     * Merge the specified Velocity template with the given model into a String.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param encoding the encoding of the template file

     * @param model the Map that contains model names as keys and model objects as values

     * @return the result as String

     * @throws TemplateEngineException if the template wasn't found or rendering failed     

     */


    public String mergeTemplateIntoString(String templateLocation, String encoding,

            Map<String, Object> model) throws TemplateEngineException {

        StringWriter result = new StringWriter();

        mergeTemplate(templateLocation, encoding, model, result);

        return result.toString();

    }

}


Вот теперь и начинается самое интересное - нам нужно создать экземпляр класса VelocityEngine и инъекцировать его в VelocityTemplateEngine. Само по себе создание объекта данного класса - задача не сложная, сложности возникают тогда, когда этот объект надо сконфигурировать. Дело в том, что для корректной работы Velocity ему необходимо указать как минимум - путь к каталогу с шаблонами, кодировку этих шаблонов и кодировку результата. Есть и ряд дополнительных параметров, но я не стал с ними заморачиватся, кому интересно сможет разобраться, посмотрев на конфигурацию кодировок. Замечу, что в проекте Spring Framework существует фабрика VelocityEngineFactory, которую мы и возьмем за основу нашего Guice-конфигуратора.

Сразу разберемся с терминологией. В терминологии Guice фабрика объектов называется провайдером. Про механизм расширения Guice-провайдеров я уже писал ранее. Сейчас мы попробуем написать провайдер, который создает объект класса VelocityEngine, используя конфигурации заданные с помощью инъекции констант (в Spring Framework используется xml). Повторюсь, что в данной реализации настраивается только путь к каталогу с шаблонами, кодировка входных файлов, кодировка результата, остальное впринципе не сложно добавить. Начнем с кода провайдера:

package ru.sposition.web.view.velocity.provider;



import java.io.IOException;

import java.util.HashMap;



import org.apache.velocity.app.VelocityEngine;

import org.apache.velocity.exception.VelocityException;

import org.apache.velocity.runtime.RuntimeConstants;



import ru.sposition.web.view.velocity.provider.annotations.InputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.OutputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.ResourceLoaderPath;



import com.google.inject.Inject;

import com.google.inject.Provider;



public class VelocityEngineProvider implements Provider<VelocityEngine> {



    @Inject

    @ResourceLoaderPath

    private String resourceLoaderPath;

   

    private HashMap<String, String> properties = new HashMap<String, String>();

   

    @Inject

    public void injectInputEncoding(@InputEncoding String inputEncoding) {

        properties.put("input.encoding", inputEncoding);

    }

   

    @Inject

    public void injectOutputEncoding(@OutputEncoding String outputEncoding) {

        properties.put("output.encoding", outputEncoding);

    }

       

    public VelocityEngine get() {

        try {

            VelocityEngine velocityEngine = newVelocityEngine();

            if (resourceLoaderPath != null)

                initVelocityResourceLoader(velocityEngine, resourceLoaderPath);

           

            for (String property : properties.keySet()) {

                velocityEngine.setProperty(property, properties.get(property));

            }

       

            velocityEngine.init();

           

            return velocityEngine;

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }



    protected VelocityEngine newVelocityEngine() throws IOException, VelocityException {

        return new VelocityEngine();

    }

   

    protected void initVelocityResourceLoader(VelocityEngine engine, String resourceLoaderPath) {

        engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");        

        engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");

        engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, resourceLoaderPath);

    }

   

    public static Provider<VelocityEngine> initProvider() {

        return new VelocityEngineProvider();

    }

}

 


Основа кода провайдера - метод get(). В данном методе создается новый экземпляр класса VelocityEnginе и заполняется параметрами, которые инъектируются в класс. Инъекция параметров осуществляется с помощью соответствующих методов и фактически просиходит добавление ЗНАЧЕНИЙ параметров в специальный Map. Данный Map хранит соответствие имен параметров в терминологии VelocityEngine и их значений. Соответствие имен и значений задается аннотациями: @ResourceLoaderPath, @InputEncoding, @OutputEncoding. Данные аннотации - ничто иное как стандартные биндинг-аннотации. Все они имеют весьма тривиальный код, который покажу на примере @InputEncoding:

package ru.sposition.web.view.velocity.provider.annotations;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



import com.google.inject.BindingAnnotation;



@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.FIELD, ElementType.PARAMETER})

@BindingAnnotation

public @interface InputEncoding {



}

 


Про настройку инъектирования с помощью аннотаций говорилось здесь.

Небольшое замечание по поводу параметра resourceLoaderPath. В ДАННОЙ реализации он должен быть равен АБСОЛЮТНОМУ пути к каталогу с шаблонами. В фабрике из Spring Framework такого ограничения нет, но там и более сложный процесс разрешения этого пути с использованием специальных лоадеров. Тянуть их из Spring Framework я посчитал нецелесообразным.

Ну и теперь пришло время собрать все эти механизмы в отдельный модуль - ViewModule (view - вид, в терминах MVC). Код модуля будет следующий:

package ru.sposition.web.view;



import org.apache.velocity.app.VelocityEngine;



import ru.sposition.web.view.annotations.TemplateEngine;

import ru.sposition.web.view.velocity.VelocityTemplateEngine;

import ru.sposition.web.view.velocity.annotations.Velocity;

import ru.sposition.web.view.velocity.provider.VelocityEngineProvider;

import ru.sposition.web.view.velocity.provider.annotations.InputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.OutputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.ResourceLoaderPath;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;



public class ViewModule extends AbstractModule {

   

    private static final String RESOURCE_LOADER_PATH = "/WEB-INF/velocity/";

    private static final String INPUT_ENCODING = "utf-8";

    private static final String OUTPUT_ENCODING = "utf-8";

   

    private final String resourcePathPrefix;

   

    public ViewModule(String resourcePathPrefix) {

        this.resourcePathPrefix = resourcePathPrefix;

    }

   

    @Override

    protected void configure() {

        bindConstant()

            .annotatedWith(ResourceLoaderPath.class)

            .to(resourcePathPrefix + RESOURCE_LOADER_PATH);

       

        bindConstant()

            .annotatedWith(InputEncoding.class)

            .to(INPUT_ENCODING);

       

        bindConstant()

            .annotatedWith(OutputEncoding.class)

            .to(OUTPUT_ENCODING);

       

        bind(VelocityEngine.class)

            .annotatedWith(Velocity.class)

            .toProvider(VelocityEngineProvider.initProvider())

            .in(Scopes.SINGLETON);;

       

        bind(ITemplateEngine.class)

            .annotatedWith(TemplateEngine.class)

            .to(VelocityTemplateEngine.class)

            .in(Scopes.SINGLETON);

    }

}

 


Как видим в конструкторе данный модуль получает параметр resourcePathPrefix - это префикс к каталогу /WEB-INF/velocity/ в файловой системе, фактически это каталог, в котором находится наше веб-приложение. Получать этот префикс мы будем с помощью сервлет-апи. В случае если пишется не веб-приложение, а, например, десктоп-приложение, получать каталог в котором оно установлено придется системными средствами.

Ну и продемонстрируем использование созданного механизма. Создадим несложный сервлет, который парсит Velocity-шаблон и выводит результат в окно браузера. Код сервлета следующий:

package ru.sposition.web.controller;



import java.io.IOException;

import java.util.HashMap;

import java.util.Map;



import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



import ru.sposition.web.view.ITemplateEngine;

import ru.sposition.web.view.annotations.TemplateEngine;

import ru.sposition.web.view.exception.TemplateEngineException;



import com.google.inject.Inject;

import com.google.inject.servlet.InjectedHttpServlet;



@SuppressWarnings("serial")

public class VelocityDemoServlet extends InjectedHttpServlet {

   

    private ITemplateEngine templateEngine;



    @Inject

    public void injectTemplateEngine(@TemplateEngine ITemplateEngine engine) {     

        templateEngine = engine;

    }



    @Override

    protected void service(HttpServletRequest request, HttpServletResponse response)

            throws ServletException, IOException {



        Map<String, Object> model = new HashMap<String, Object>();

        model.put("hello", "Welcome to Hell!");

        model.put("title", "Demo servlet title");

       

        response.setContentType("text/html; charset=UTF-8");

        try {

            templateEngine.mergeTemplate("index.vm", model, response.getWriter());

        } catch (TemplateEngineException e) {      

            e.printStackTrace();

            throw new ServletException(e.getMessage());

        }

    }

}

 


Как видим сервлет даже не знает ничего о Velocity, он инъектирует шаблонный движок (ITemplateEngine) и его уже использует. Напомню, что для разрешения зависимостей сервлету нужен Injector, который он пытается взять из своего контекста. Создается Injector и помещается в контекст-сервлета с помощью ServletContextListener'а:

package ru.sposition.web.controller;



import javax.servlet.ServletContext;

import javax.servlet.ServletContextEvent;

import javax.servlet.ServletContextListener;



import ru.sposition.web.view.ViewModule;



import com.google.inject.Guice;

import com.google.inject.Injector;

import com.google.inject.Module;

import com.google.inject.servlet.ServletModule;



public class VelocityDemoServletContextListener implements ServletContextListener {

   

    public void contextInitialized(ServletContextEvent servletContextEvent) {

        ServletContext servletContext = servletContextEvent.getServletContext();

        servletContext.setAttribute(Injector.class.getName(), getInjector(servletContext));

    }

   

    public void contextDestroyed(ServletContextEvent servletContextEvent) {

        ServletContext servletContext = servletContextEvent.getServletContext();

        servletContext.removeAttribute(Injector.class.getName());

    }

       

    protected Injector getInjector(ServletContext servletContext) {    

        return Guice.createInjector(new Module[] {new ServletModule(),

                new ViewModule(servletContext.getRealPath("/"))});

    }

}

 


Код servletContext.getRealPath("/") как раз таки и служит для получения каталога, в который установлено веб-приложение.

Ну и напоследок - код демонстрационного шаблона:

<?xml version="1.0" encoding="windows-1251"?>

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">

    <head>

        <title>${title}</title>

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>

        <link rel="stylesheet" type="text/css" href="/style.css" />    

    </head>

    <body>

        <h1>${hello}</h1>

    </body>

</html>    

 


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

Понравилось сообщение - подпишись на блог

4 комментария:

iZEN комментирует...

Приветствую.
Выразите, пожалуйста, Ваше отношение к Velocity vs. JSF. В каких вещах каждая из этих двух технологий превосходит друг друга.

Я так понял, что Velocity просто шаблонный движок, надстройка над JSP.
JSF это не только надстройка над JSP, но и компонентная технология, предоставляющая возможность управлять переходами между страницами, включать сложные валидаторы данных (в том числе подключать другие фреймворки), и аправлять рендерингом элементов на страницах.

В чём суть ажиотажа вокруг Velocity? Ведь то же самое можно делать и в JSF.

Samolisov Pavel комментирует...

Здравствуйте, большое спасибо за ваш вопрос. Попробую на него ответить.

Velocity это шаблонный движок, но надстройкой над JSP он не является, он вообще не имеет никакого отношения к JSP. Вообще Velocity можно использовать не только как View в веб-приложениях, но и везде где требуется генерация отчетов, платежек и прочей нечести. В частности в Naumen DMS мы используем Velocity для генерации почтовых сообщений.

Что касается JSF vs Velocity тут сравнение на мой взгляд не совсем корректное. Все таки JSF это полноценный веб-фреймворк, в то время как Velocity лишь средство отображения. Но JSF слишком тяжеловесный для многих задач. На мой взгляд банальную гостевуху на сайте проще писать все же используя Velocity

searoso комментирует...

Подскажите, плс, кому можно обратиться чтобы шаблон с Joomla переписали на Velocity. Переезжаем… =)

Pavel Samolisov комментирует...

Думаю вам стоит обратиться к любому Java-программисту фрилансеру.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!