Tagged: Groovy, Grails and GORM

Human readable past date formating in Grails

Sometimes it better to write ‘2 days ago’ instad of ’28 Jan 2015′.
You can find a lot of solution in How to calculate “time ago” in Java?.

Here is a simplest snippet for making it in Grails from my project with i18n support.
Just create new tag `timeAgo` in your project taglib:

    static encodeAsForTags = [ timeAgo: [taglib:'html'] ]

    final static long ONE_SECOND = 1000;
    final static long ONE_MINUTE = ONE_SECOND * 60;
    final static long ONE_HOUR = ONE_MINUTE * 60;
    final static long ONE_DAY = ONE_HOUR * 24;

    /**
     Converts time (in milliseconds) to human-readable format
     "<w> days, <x> hours, <y> minutes ago" or "just now"
     <code>
     System.out.println(millisToLongDHMS(123));
     System.out.println(millisToLongDHMS((5 * ONE_SECOND) + 123));
     System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR));
     System.out.println(millisToLongDHMS(ONE_DAY + 2 * ONE_SECOND));
     System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR + (2 * ONE_MINUTE)));
     System.out.println(millisToLongDHMS((4 * ONE_DAY) + (3 * ONE_HOUR) + (2 * ONE_MINUTE) + ONE_SECOND));
     System.out.println(millisToLongDHMS(42 * ONE_DAY));
     </code>
     output :
     0 second
     5 seconds
     1 day, 1 hour
     1 day and 2 seconds
     1 day, 1 hour, 2 minutes
     4 days, 3 hours, 2 minutes
     42 days
     */
    private String millisToLongDHMS(long duration) {
        StringBuffer res = new StringBuffer();
        long temp;
        if (duration >= ONE_MINUTE) {
            temp = duration / ONE_DAY;
            if (temp > 0) {
                duration -= temp * ONE_DAY;
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.days') : g.message(code: 'timeAgo.day')).append(duration >= ONE_MINUTE ? ', ' : '')
            }
            temp = duration / ONE_HOUR;
            if (temp > 0) {
                duration -= temp * ONE_HOUR;
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.hours') : g.message(code: 'timeAgo.hour')).append(duration >= ONE_MINUTE ? ', ' : '')
            }
            temp = duration / ONE_MINUTE
            if (temp > 0) {
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.minutes') : g.message(code: 'timeAgo.minute'))
            }
            res.append(' ').append(g.message(code: 'timeAgo.ago'))
            return res.toString()
        } else {
            return g.message(code: 'timeAgo.justNow')
        }
    }

    /**
     * @emptyTag
     *
     * @attr date the date in past
     */
    def timeAgo = { attrs ->
        Date date = attrs.date
        out << millisToLongDHMS((new Date().getTime()) - date.getTime());
    }

And then you need to add message codes in `messages.properties`:

timeAgo.justNow=just now
timeAgo.minute=minute
timeAgo.minutes=minutes
timeAgo.hour=hour
timeAgo.hours=hours
timeAgo.day=day
timeAgo.days=days
timeAgo.ago=ago
# for Russian
timeAgo.justNow=только что
timeAgo.minute=минуту
timeAgo.minutes=минут
timeAgo.hour=час
timeAgo.hours=часов
timeAgo.day=день
timeAgo.days=дней
timeAgo.ago=назад
# for Ukrainian
timeAgo.justNow=тіки що
timeAgo.minute=хвилину
timeAgo.minutes=хвилин
timeAgo.hour=годину
timeAgo.hours=годин
timeAgo.day=день
timeAgo.days=днів
timeAgo.ago=тому
Реклама

«Comparing JVM Web Frameworks» by Matt Raible

It’s very cool talk about qestions that every Java developer must decide on new project. Overview of all popular Java frameworks from JSF and Struts to Play and Vaadin.

[Grails] Избегайте использования Environment вне файлов конфигураций

Внутри Grails есть великолепный механизм для условного выполнения кода в зависимости от текущей среды (Environment).
Например внутри DataSource.groovy можно указывать различные настройки базы данных:

// environment specific settings
environments {
    development {
        dataSource {
            dbCreate = "create-drop"
            url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
    production {
        dataSource {
            dbCreate = "update"
            url = "jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
}

Наверняка у вас почти в каждом файле конфигурации есть настройки под среду.

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

<g:if env="test"> ... </g:if>

Я постепенно пришёл к тому что этого следует избегать, потому что теряется читаемость и гибкость. Вместо этого лучше явно создать опцию в Config.groovy, включать или выключать её в зависимости от среды а и потом проверять её. Вот например что делает этот код?

    <g:if env="test">
        <meta name="controller" content="${controllerName}"/>
        <meta name="action" content="${actionName}"/>
    </g:if>

На самом деле этот код добавляет имя контроллера и экшена которые отрисовли страницу и они потом проверяются функциональным тестом. Это это бывает очень удобно для дебага.
А теперь давайте мы создадим в файле Config.groovy опцию

environments {
    development {
        com.example.showActionNameInPageMeta = true
    }
    test {
        com.example.showActionNameInPageMeta = true
    }
    production {
        com.example.showActionNameInPageMeta = false
    }
}

и теперь

    <g:if test="${grailsApplication.config.com.example.showActionNameInPageMeta}">
        <meta name="controller" content="${controllerName}"/>
        <meta name="action" content="${actionName}"/>
    </g:if>

В результате эта опция улучшила читаемость кода. Кроме того теперь мы всегда можем добавить новое окружение, например testFunctional для функциональных тестов и внутри включить нашу опцию, при этом не переписывая код.
И что особенно важно, что в случае каких то проблем в продакшене сисадмин может поменять конфигурацию и перезапустить сервер не перекомпилируя приложение.
Кроме того, все такие опции теперь не размазаны по коду а лежат в одном месте где их все можно посмотреть.

В более широком смысле такой подход называется Feature flag, и его активно используют например в Amazon.

[Grails] favicon.ico и robots.txt

Есть две важные мелочи на которые обычно внимания не обращаем, но потом приходится всё таки с ними повозится.

Где должна быть иконка для закладки?

Например favicon.ico — очень важная маленькая вещь. Иконка значительно увеличивает узнаваемость закладки.
По умолчанию в main layout который генерирует Grails есть прописанный путь к favicon:

<html>
<head>
    ...
    <link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon">
...
</head>

Броузер должен понять что фавикон лежит по URL /static/images/favicon.ico

Оказалось что броузеры не любят когда фавикон находится не в корне сайта, т.е. не /favicon.ico. Например если сайт загружен через фрейм или просто открывается картинка с сайта то некоторые броузеры пытаются запросить фавикон с корня сайта.

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

Как решать?

Первое что приходит на ум это прописать в UrlMappings перенаправление на правильный путь к иконке. Но конечно же не стоит этого делать, ведь мы помним что всё что лежит в папке web-app становится доступным по прямо ссылке.
Так что нам достаточно просто перенести фавикон в директорию уровнем выше:

mv grails-app/web-app/images/favicon.ico grails-app/web-app/favicon.ico

И ещё подправить наш лейаут grails-app/views/layouts/main.gsp

<html>
<head>
    ...
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
...
</head>

Теперь фавкион доступен прямо из корня сайта /favicon.ico.
На форумах пишут что на мобильные устройства ещё запрашивают свои иконки в другом разрешении, но у меня сейчас по рукой их нет чтобы проверить.

Защищаемся от поисковика

Наверное многие помнят историю когда все SMS отправленные с сайта Билайна стали доступны через поисковик яндекса. Это произошло по стечению обстоятельств, одним из которых было то что разработчики не запретили поисковикам индексировать страницы отправки СМС.
Чтобы себя уберечь вам следует тоже запретить поисковикам индексировтаь админку вашего сайта или другие приватные разделы.
Для этого в корень вашего сайта вы должны разметсить файл robots.txt с примерно таким содержимым:

User-agent: *
Disallow: /
Allow: /faq/

Поисковики когда будут индексировать ваш сайт будут соблюдать эти правила и не будут индексировать весь ваш сайт кроме страницы /faq/

Как не сложно догадаться файл robots.txt тоже нужно положить в web-app чтобы он стал доступен по прямой ссылке.

Если вам уже не достаточно простых настроек robots.txt то вам стоит обратить внимание на его XML развитие Sitemaps.
Для Grails даже есть плагин grails-sitemapper, но он выглядит заброшенным так что десять раз перепроверьте его содержимое.

По хорошему эти две вещи нужно сделать частью стандартного Grails. Как нибудь создам тикет в трекере на эту тему.

[Grails] SEQUENCE generator name

Если у доменного объекта не указан способ генерации айдишника тогда Hibernate подбирает его сам. Это называется native стратегия.

Если вы используете не уродскую MySQL, а настоящую базу данных типа PostgreSQL, Oracle или Firebird, то генерация айдишников будет происходить через SEQUENCE.

При генерации БД название для сиквенсов генерируется автоматически и выглядят они примерно так: SYSTEM_SEQUENCE_99A7416D_98E6_485C_8568_1DD6C99BF31C. Т.е. нейминг стратегия мягко говоря неадекватная.
Из-за неё возникает несколько проблем.

Во первых из названия такого сиквенса не понятно для какой именно он таблицы.

Во вторых, я сталкивался с проблемой когда при следующем релизе через database migration накатываются изменения и возникает необходимость сбросить сиквенс, то не ясно по какому имени к нему обращаться.

Как решать?

Переопределить нейминг

Первый вариант создать свой диалект внутри которого прописать правильную генерацию имени сиквенса.
По уму стоило бы всё таки пропатчить эту паскудную генерацию имени сиквенсов в самом хибернейте. Ставлю пивашку тому кто это сделает 🙂

Указать имя сиквенса явно

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

class Book {
    String title

    static mapping = {
        id generator: 'sequence', params: [sequence: 'book_seq']
    }
}

Теперь для таблицы book будет создан сиквенс book_seq.

Естественно, если вы вдруг решите запустить приложение на БД в которой нет сикенсов то отгребёте ошибку типа такой org.hibernate.MappingException: org.hibernate.dialect.MySQL5InnoDBDialect does not support sequences.

Так что такое решение не годится для использования если вам не известная используемая БД, например в плагинах.

Правильное решение

Я сначала подумал что на этом тупик, но пролистав документацию хибернейта дальше обнаружил что решение уже есть.
Если вкратце, то начиная с версии 3.2.3 в хибернейте появился SequenceStyleGenerator который в отличии от нейтив, позволяет указать опции одновременно и для SEQUENCE генератора и для Table не падает с ошибкой на старте.

class Book {
    String title

    static mapping = {
        id generator: 'org.hibernate.id.enhanced.SequenceStyleGenerator', params: [sequence_name: 'book_seq']
    }
}

В документации сказано что ещё следует включить новые генераторы. Для этого в DataSource.groovy нужно добавить опцию

hibernate.id.new_generator_mappings = true

Но если честно, то всё и без этого заработало 🙂

Теперь в MySql рядом с таблицей book создалась таблица book_seq с одной строкой и одним полем next_val которая имитирует сиквенс.

[Grails] i18n: LocaleResolver, Accept-Language

Warning!!! I made plugin grails-locale-configuration-plugin that replace this solution. [Grails] Сегодня опубликовал стабильную версию grails-locale-configuration-plugin
Also this chapter of book may be interest for you

В каждом броузере пользователь может настроить предпочтительные языки. Например в хроме Settings / Languages (chrome://settings/languages) они выглядят так:
chrome languages settings
Тут указано что пользователь хочет видеть американский английский, или любой английский, но если его не будет тогда на русском, а если и на русском не будет, тогда давайте уже на украинском.

Эти параметры передаются в каждом запросе на сервер через заголовок Accept-Language:

Accept-Language:en-US,en;q=0.8,ru;q=0.6,uk;q=0.4

С помощью параметра q (quality value) мы передаём приоритет от 0 до 1.

Grails умеет работать с этим заголовком и автоматически переключать локаль. Если есть интернационализация для этого языка то она сразу же автоматически показывается и всё хорошо. А вот если не находится i18n/messages_xx.properties для нужной локали, тогда отображается текст по умолчанию из i18n/messages.properties, обычно английский.

Хоть текст и будет английский, но системная локаль будет установлена в ту которую пользователь запросил больше всего.
Например, у нас есть пользователь у которого украинский язык на первом месте и русский на втором.
Запросив страницу Grails запомнит украинскую локаль в сессии, но отобразит всё на английском.
Это не всегда хорошо, особенно если ваш сайт жёстко поддерживает только несколько локалей. Например в зависимости от локали вы разные флажки отображаете.
Скорее всего что захотите ограничить локали которые поддерживаете.
Для этого создайте в файле Config.groovy опцию supportedLocales:

supportedLocales = [Locale.ENGLISH, new Locale('RU')]

А теперь создадим фильтр который будет проверять локаль

class LocaleResolverFilterFilters {

    def filters = {
        all(controller: '*', action: '*') {
            before = {
                // Сначала ищем такую же локаль, если не нашли то локаль с тем же языком, если не нашли то по умолчанию английский
                Locale selectedLocale
                LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request)
                List&amp;amp;amp;lt;Locale&amp;amp;amp;gt; supportedLocales = grailsApplication.config.supportedLocales
                if (request.locale in supportedLocales) {
                    selectedLocale = request.locale
                } else {
                    selectedLocale = findLocaleWithSameLanguage(request, supportedLocales)
                }
                selectedLocale = selectedLocale ?: Locale.ENGLISH
                localeResolver.setLocale(request, response, selectedLocale)
            }
        }
    }

    private Locale findLocaleWithSameLanguage(HttpServletRequest request, List&amp;amp;amp;lt;Locale&amp;amp;amp;gt; supportedLocales) {
        supportedLocales.find({ it.language == request.locale.language })
    }
}

В дальнейшем из кода получить локаль мы можем через объект запроса request.locale а список всех локалей предпочитаемых пользователем через request.locales.
Ту локаль с которой мы отрисовали страницу можно увидеть через объект ответа: response.locale.
Смотрите демо приложение на гитхабе.

Grails request.locale

Что ещё почитать по теме:

[Grails] toString() method on domain class with id in output

Groovy has a cool annotation @ToString.
But I found strange behavior when trying to include id field to output of toString().
For example we have some domain class that marked with @ToString:

@ToString(includeNames = true, includeFields = true,  includes = ['id'])
class User {
...
}

def user = new User()
user.id = 1 // id can't be bonded as a param of constructor, that's why we should set it directly
// Let's test generated toString()!
assert == 'User(id:1)' // assert fails!

But this assertion will fail: toString() skips id field. Maybe it happens because of id field is not declared explicitly in User class. It added implicitly by Grails.
I found workaround — just declare id field explicitly:

@ToString(includeNames = true, includeFields = true,  includes = ['id'])
class User {
    Long id
...
}

def user = new User()
user.id = 1
assert == 'User(id:1)' // assert successful 🙂

I dont know is this a bug or feature, but hope this workaround will help you.