Введение
Для передачи данных от интерфейса веб-приложения есть несколько методов, но, пожалуй, самый распространенный — отправка формы с MIME-type application/x-www-form-urlencoded
. Ещё один вариант — multipart/form-data
.
В качестве серверной технологии для приема могут использоваться контроллеры в MVC-фреймворках (из основных Java-технологий следует упомянуть Spring MVC, Java Server Pages, Java Server Faces. Но эти фреймворки довольно сильно осложняют жизнь разработчику интерфейса, если он не знаком с Java или нужен шаг в сторону от того, что позволяет фреймворк. В случае же экспонирования REST-интерфейса бэкендом приложения разработкой фронта упрощается: ей может заниматься человек, знающий базовый javascript и jquery, независимый от разработки бэка. Кроме того, даже при использовании шаблонизатора, выбор сильно расширяется: Apache Velocity, FreeMarker (стоит упомянуть, что Spirng MVC неплохо интегрируется с последними). Тогда данные из формы на стороне сервера записываются в бины, с которыми связан данный view/controller. Правда, у JSF тоже наблюдалась генетическая проблема с кодировками, рассмотрение которой — тема для отдельной статьи.
Краткое введение в JAX-RS было дано в предыдущей статье. И экспонированный через JAX-RS интерфейс может принимать GET
и POST
запросы с данными форм. О проблемах данного подхода при использовании не-latin-1
и пойдет речь в данной статье.
Получение данных формы в JAX-RS
Для инжекции парамтра из формы в метод существует несколько аннотаций: @Form
, @FormParam
. Для POST
-запроса @FormParam
эквивалентен @QueryParam
для GET
по поведению. С маленьким исключением: поведение при использовании utf-8. В случае использования метода GET
декодирование в строку из urlencoded
происходит без проблем. Для POST
-же в Content-Type
требуется указать charset=utf-8
, чтобы при декодировании из urlencoded
преобразование байтового потока происходило в UTF-8, а не Latin-1 (поведение по умолчанию).
Браузеры
Браузеры (или jquery, например) не указывают кодировку, что приводит к неправильной интерпретации значений полей формы, если они содержат старшие CP UTF-8. Обходится данная проблема при использовании jquery явным указанием заголовка Content-Type: application/x-www-form-urlencoded; charset=utf-8
. Для формы без js мне не удалось достичь успеха указанием enctype для формы.
JBoss Resteasy
Возникает, конечно, вопрос: надо ли использовать аннотацию @Form, если данные будут приходить и от других приложений? В нашем случае её использование оправдано из принципов DRY: один и тот же метод может использоваться и формой клиента, и внешними приложениями, использующими это API.
При использовании клиентского фрейморка Resteasy возможны несколько вариантов. Например, можно добавить в необходимые методы @HeaderParam("Content-Type")
:
@Path("/")
public interface Rest {
@POST
@Path("test")
public String test(@FormParam("q") String query, @HeaderParam("Content-Type") String contentType);
}
Использование тогда вышлядит следующим образом:
Rest client = ProxyFactory.create(Rest.class, url);
client.test(query, "application/x-www-form-urlencode; charset=utf-8");
Но более правильным и удобным будет использование клиентского интерсептора, добавляющего в заголовок Content-Type: application/x-www-form-urlencoded
поле charset
. Реализуется это следующим образом:
@ClientInterceptor
@HeaderDecoratorPrecedence
public class RestInterceptor implements ClientExecutionInterceptor {
public static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
public static final String FORM_CONTENT_TYPE_WITH_CHARSET = "application/x-www-form-urlencoded; charset=utf-8";
@Override
public ClientResponse execute(ClientExecutionContext context) throws Exception {
String contentType = context.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
if (formWithoutCharset(contentType)) {
context.getRequest().header(HttpHeaders.CONTENT_TYPE, FORM_CONTENT_TYPE_WITH_CHARSET);
}
return context.proceed();
}
private boolean formWithoutCharset(String contentType) {
return contentType != null
&& contentType.contains(FORM_CONTENT_TYPE)
&& ! contentType.contains("charset");
}
}
Такой вариант интерсептора, конечно, не идеален. Возможно, что действительно хочется отправить форму в latin-1
… Но это первый шаг к тому, чтобы упростить код клиента.
Для его активации необходимо немного поправить процедуру инициализации фреймворка:
public static void initResteasy() {
ResteasyProviderFactory factory = ResteasyProviderFactory.getInstance();
RegisterBuiltin.register(factory);
InterceptorRegistry<ClientExecutionInterceptor> registry = factory.getClientExecutionInterceptorRegistry();
registry.register(new RestInterceptor());
}
После этого и при использовании ClientRequest
, и при использовании прокси-объектов в процессе отправки запроса при наличии заголовка Content-Type
с application/x-www-form-urlencoded
, но без charset
, то проставляется заголовок, его содержащий.
Автор: grossws