Статьи

Контекстно-зависимые записи набора ресурсов в приложениях JSF

Перед нами стояла интересная проблема: наше JSF-приложение должно отображать стандартный текст — заголовки, метки кнопок, подсказки, сообщения об ошибках, подсказки и т. Д. — контекстно-зависимым образом. Не только по языку, региону и варианту — хорошо известные измерения, по которым стандартный механизм JSF и Java работает с Resource Bundles. Помимо этой простой чувствительности к «локали» — которая нам также нужна — нам нужна более специализированная контекстная зависимость. В нескольких измерениях.

Например, когда пользователь младшей возрастной категории обращается к веб-приложению, представленный текст должен (или, по крайней мере, может) отличаться от того, что мы показываем нашим старшим пользователям. Кроме того, когда к приложению обращаются в контексте определенного бренда или компании, текст может отличаться от контекста другого бренда или компании. А маркетинговый отдел пришел к выводу, что может представлять текст, адаптированный к времени года — зима или лето, сезон отпусков или отсутствие рождества — или день недели — рабочий день или выходные. Старый добрый отдел маркетинга — если бы они правили миром ….

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

Мы начали с внимательного изучения механизма по умолчанию в JSF 1.2. А потом взял это оттуда.

Средства ResourceBundle по умолчанию в JavaServer Faces 1.2

Средства по умолчанию довольно просты:

— ResourceBundles настраиваются либо для каждой страницы, либо для приложения в целом (по состоянию на JSF 1.2). Последнее обычно предпочтительнее, так как указание комплекта ресурсов для каждой страницы является довольно сложной задачей.

Конфигурация ResourceBundle в face-config.xml выглядит следующим образом:

<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee">
<application>
<resource-bundle>
<base-name>nl.amis.appBundle</base-name>
<var>msg</var>
</resource-bundle>
</application>

 Здесь base-name относится к файлу свойств в файловой системе. Обратите внимание, что вместо файла свойств мы также можем использовать класс, который может дать некоторый контроль над кодировкой и специальными символами в сообщениях и / или тем, откуда извлекать сообщения (например, из базы данных).

Файл свойств в этом случае предельно прост:

title=The Interesting World of Internationalization
formSubmit=Apply Changes if you like
ageCategorySelector=Select Age Category

Всего три клавиши с соответствующими сообщениями. На данный момент нет определенных для локали версий пакета — так что нет файлов appBundle_en_us., Properties или appBundle_nl.properties.

Имея конфигурацию вface-config.xml и файл свойств с ключами и сообщениями, мы можем создать страницу JSF, которая использует сообщения из пакета. Обратите внимание, что JSF заботится о применении правильного языкового стандарта — читайте из ViewRoot (FacesContext.getCurrentInstance (). GetViewRoot (). GetLocale ()).

Очень простая страница JSF выглядит следующим образом:

 ...
<f:view>
<html>
...
<body>
<h:form>
<h:panelGrid columns="1" >
<h:outputText value="#{msg.title}"/>
<h:commandButton value="#{msg.formSubmit}"/>
</h:panelGrid>
</h:form>
</body>
</html>
</f:view>

 

Обратите внимание на ссылки на сообщения i18n-ed, использующие выражения EL в формате # {msg [‘key’]} или эквивалентный # {msg.key}. Пакет ресурсов был зарегистрирован с переменной msg, на которую мы можем ссылаться в этих выражениях.

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

 

 

Настройка этапа для контекстно-зависимой обработки ресурсов — перехват запросов ResourceBundle

Пока ничего особенного. Но мы должны начать готовиться к влиянию контекста. Мы должны спросить себя: как мы можем позволить контексту влиять на то, как сообщения извлекаются из пакета ресурсов. Конечно, механизм связывания ресурсов JSF и Java по умолчанию не знает ничего, кроме простого старого Locale. Мы должны предварительно обработать сообщение-запрос к пакету ресурсов, чтобы обработать контекстную чувствительность. Таким образом, мы должны перехватить запрос от приложения до того, как будет запущен механизм пакета ресурсов.

Ну, это достаточно просто. Вызовы пакета ресурсов задаются через выражения EL, например # {msg.key}. Если мы убедимся, что msg больше не сам пакет ресурсов, а наш собственный управляемый компонент, который может реализовывать интерфейс Map — для обработки запроса .key — мы достигли перехвата.

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

Давайте начнем с простого перехвата, без суеты вообще.

Конфигурация olur управляемых bean-компонентов — обратите внимание, что есть два bean-компонента, один для реализации Map и перехвата запроса сообщения (MessageProvider), а другой для реализации перехвата и применения логики чувствительности к контексту:

<managed-bean>
<managed-bean-name>msgMgr</managed-bean-name>
<managed-bean-class>nl.amis.MessageManager</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>msg</managed-bean-name>
<managed-bean-class>nl.amis.MessageProvider</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>msgMgr</property-name>
<value>#{msgMgr}</value>
</managed-property>
</managed-bean>

Теперь, когда мы
похитили имя msg для нашего MessageProvider, мы должны зарегистрировать сам Resource Bundle под другим именем.

<application>
<resource-bundle>
<base-name>nl.amis.appBundle</base-name>
<var>msgbundle</var>
</resource-bundle>
</application>
 The implementation of  the MessageProvider class is very simple - it pass the request onwards to the MessageManager:
package nl.amis;

import java.util.HashMap;

public class MessageProvider extends HashMap {

private MessageManager msgMgr;
public MessageProvider() {
}

@Override
public Object get(Object key) {
return msgMgr.getMessage((String)key);
}

public void setMsgMgr(MessageManager msgMgr) {
this.msgMgr = msgMgr;
}

public MessageManager getMsgMgr() {
return msgMgr;
}
}

Ссылки на страницах JSF могут оставаться неизменными: не имеет значения, ссылается ли выражение EL на странице на Resource Bundle, зарегистрированный непосредственно в JSF, или на управляемый компонент, как мы делаем здесь.

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

public class MessageManager {


public String getMessage(String key) {

// use standard JSF Resource Bundle mechanism
return getMessageFromJSFBundle(key);

// use the default Java ResourceBund;e mechanism
// return getMessageFromResourceBundle(key);
}

private String getMessageFromResourceBundle(String key) {
ResourceBundle bundle = null;
String bundleName =
"nl.amis.appBundle";
String message = "";
Locale locale =
FacesContext.getCurrentInstance().getViewRoot().getLocale();
try {
bundle =
ResourceBundle.getBundle(bundleName, locale, getCurrentLoader(bundleName));
} catch (MissingResourceException e) {
// bundle with this name not found;
}
if (bundle == null)
return null;
try {
message = bundle.getString(key);
} catch (Exception e) {
}
return message;

}


private String getMessageFromJSFBundle(String key) {
return (String)resolveExpression("#{msgbundle['" + key + "']}");
}

public static ClassLoader getCurrentLoader(Object fallbackClass) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null)
loader = fallbackClass.getClass().getClassLoader();
return loader;
}

// from JSFUtils in Oracle ADF 11g Storefront Demo
public static Object resolveExpression(String expression) {
FacesContext facesContext = FacesContext.getCurrentInstance();
Application app = facesContext.getApplication();
ExpressionFactory elFactory = app.getExpressionFactory();
ELContext elContext = facesContext.getELContext();
ValueExpression valueExp =
elFactory.createValueExpression(elContext, expression,
Object.class);
return valueExp.getValue(elContext);
}
}

Мы можем выбрать один из двух способов доступа к Resource Bundle.

Одним из них является непосредственное использование механизма JSF, который будет работать только для комплектов ресурсов, которые явно зарегистрированы в JSF в файлахface-config.xml. Смотрите метод getMessageFromJSFBundle.

Другой вариант охватывает все средства, которые предлагает JSF, и напрямую использует стандартную библиотеку Java ResourceBundle. При таком подходе мы должны сами выяснить локаль. Мы можем использовать этот подход с любым файлом комплекта ресурсов на пути к классам, без их явной регистрации в Face-config.xml. Этот подход используется в getMessageFromResourceBundle ().

Введите контекст, который должен влиять на результаты пакета ресурсов

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

Компонент MessageManager расширяется с помощью свойства ageCategory, а также метода получения и установки.

Страница JSF расширена с помощью элемента управления списком — который по какой-то странной причине не получает свои метки из пакета ресурсов:

<h:selectOneListbox value="#{msgMgr.ageCategory}"
label="#{msg.ageCategorySelector}">
<f:selectItem itemLabel="Junior" itemValue="junior"
itemDescription="Age category Under 35"/>
<f:selectItem itemLabel="Senior" itemValue="senior"
itemDescription="Age category 35+"/>
</h:selectOneListbox>

Теперь мы можем установить контекст. Вопрос: какая разница? Или еще лучше: как мы можем это изменить? Когда пользователь переключается со старшей на младшую возрастную категорию, что должно повлиять на текст на странице?

По сути, есть два способа справиться с этим. Одним из них является добавление дополнительных ключей в комплект ресурсов; эти ключи состоят из исходного (базового) ключа и идентификации контекста, в котором применяется ключ. Например:

title=The Interesting World of Internationalization
formSubmit=Apply Changes if you like
ageCategorySelector=Select Age Category
title_senior=The never ending wonders of the World of Internationalization
formSubmit_senior=Notify the application of your desires by pressing this button
title_junior=Speaking your own language
ageCategorySelector_junior=Pick your own age peer group

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

Требуемое изменение в коде MessageManager минимально. Сначала мы должны попытаться найти сообщение для ключа, состоящего из базового ключа (переданного со страницы) и текущего контекста. Когда сообщение не найдено, мы пытаемся снова, на этот раз только с базовым ключом.

public String getMessage(String key) {

// add the current context to the key and dive into the large resource bundle with all keys,
// simple and composed with context

// use the default Java ResourceBunde mechanism
String msg = getMessageFromResourceBundle(key+"_"+ageCategory);
if (msg==null || msg.startsWith("???"))
msg = getMessageFromResourceBundle(key);

return msg;
}

Опять же, возможны два подхода: один проходит через JSF, а другой — прямо в ResourceBundles.

Примечание: сложные ключи могут иметь некоторую причудливую иерархическую контекстную схему, которая вам нравится: junior_christmas_male_brandX_page1Title, christmas_male_brandX_page1Title, male_brandX_page1Title, brandX_page1Title, pageTitle1.

Запустить страницу; выберите Старший и нажмите кнопку:

 

 Затем выберите Junior и нажмите кнопку:

 

Resource Bundle для каждого значения контекста

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

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

В нашем примере мы можем создать файлы с именами appBundleSenior.properties и appBundleJunior.properties, например. Последний содержит:

title=Speaking your own language
formSubmit=Make it happen!
ageCategorySelector=Pick your own age peer group

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

Чтобы получить доступ к содержимому этого файла, мы должны внести небольшие изменения в MessageManager. Изменения, которые отличаются для подхода JSF Bundle или простого подхода Java ResourceBundle.

Сначала обсудим последний случай: мы можем просто получить доступ ко всем ResourceBundles на пути к классам. Если мы просто передадим другое имя, это нормально с механизмом. Мы расширяем метод getMessageFromResourceBundle вторым параметром: bundlePostFix. Этот постфикс указывает значение контекста. Постфикс просто добавляется в имя пакета, и метод пытается найти сообщение:

private String getMessageFromResourceBundle(String key, String bundlePostfix) {
ResourceBundle bundle = null;
String bundleName = "nl.amis.appBundle" + (bundlePostfix == null ? "" : bundlePostfix);
String message = "";
Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
try {
bundle = ResourceBundle.getBundle(bundleName, locale,
getCurrentLoader(bundleName));
} catch (MissingResourceException e) {
// bundle with this name not found;
}
if (bundle == null)
return null;
try {
message = bundle.getString(key);
} catch (Exception e) {
}
return message;
}

Поэтому небольшое изменение также требуется в методе getMessage ():

   public String getMessage(String key) {
String msg = getMessageFromResourceBundle(key, (String)ageCategory);
if (msg == null || msg.startsWith("???"))
msg = getMessageFromResourceBundle(key, "");
return msg;
}

Мы передаем постфикс — и неукрашенный ключ — методу, и когда сообщение не возвращается, мы снова пытаемся в базовом комплекте.

Используя механизм комплектации JSF, изменение является на один шаг более сложным: нам нужно зарегистрировать комплект специфичных для контекста ресурсов с помощью JSF в файлеface-config.xml:

<application>
<resource-bundle>
<base-name>nl.amis.appBundle</base-name>
<var>msgbundle</var>
</resource-bundle>
<resource-bundle>
<base-name>nl.amis.appBundleJunior</base-name>
<var>juniormsgbundle</var>
</resource-bundle>
</application>

 

Теперь мы можем переписать метод getMessageFromPrefixedBundle ():

    private String getMessageFromPrefixedBundle(String key,
String bundlePrefix) {
return (String)resolveExpression("#{" + bundlePrefix + "msgbundle['" + key + "']}");
}

Здесь мы предположили, что контекстно-зависимый идентификатор применяется в качестве префикса к имени Resource Bundler var.

Изменение в методе getMessage ():

public String getMessage(String key) {
// for the JSF ResourceBundle mechanism, assume a second <application><resource-
// bundle> entry that specifies a variable called <context identifier>msgbundle

String msg = getMessageFromPrefixedBundle(key, (String)ageCategory);
if (msg==null || msg.startsWith("???"))
msg = getMessageFromJSFBundle(key);
return msg;
}

 

Резюме

Вы видели, как можно использовать стандартный механизм Resource Bundle в JSF. Затем мы предложили способ перехвата запроса сообщения i18n в управляемом компоненте, который может манипулировать запросом с учетом текущего контекста — кроме стандартного Locale -. Мы не показали, как Resource Bundle может быть реализован классом, который либо жестко кодирует пары ключ / сообщение, либо получает их из внешнего источника, такого как база данных.

Первоначально отправленный Лукасом Джеллемой, размещенным в Технологическом блоге AMIS