Monthly Archives: April 2014

Борьба с native queries в EclipseLink 2.5 (Oracle DB)

Written by elwood

JPA Logo

Предыстория: у меня был проект – веб-приложение, работающее в GlassFish. Приложение собиралось в EAR а слой данных был реализован на базе JPA. В качестве реализации JPA использовался EclipseLink 2.5, встроенный в GlassFish 4.0. База данных – Oracle 11g Express Edition.

Вообще, до этого я всегда работал с Hibernate, и EclipseLink заставил немного помучаться с особенностями восприятия JPQL. Например, EclipseLink не хотел понимать count(*) и все используемые таблицы обязывал снабжать алиасами. Но это мелочи, всё было хорошо, пока не понадобилось сделать несложный запрос: получить объекты с условием, в котором было необходимо применение арифметики с датами. Итак, краткая история мучений.

JPQL: OPERATOR()

Вычитал, что в EclipseLink JPQL можно использовать оператор OPERATOR('AddDate') тынц. Как оказалось, в EclipseLink этот оператор реализован не был. Что в принципе удивительно, т.к. вендором EclipseLink сейчас является Oracle, и нормальный человек в этом случае имеет все основания ожидать хорошей поддержки их СУБД.

JPQL: SQL()

Попробовал также и SQL(), позволяющий вставлять в JPQL вставки нативного SQL тынц. К сожалению, и тут не получилось. Результирующий SQL был с багом, говорящем о несоответствии количества открывающих и закрывающих скобок.

Пробуем @NamedNativeQuery

Отчаявшись решить проблему в рамках JPQL, решил спуститься на ступеньку ниже и заюзать Native Query. Но запрос мой требовал соединения четырёх сущностей, и некоторые поля в них назывались по умолчанию одинаково (ID). Аннотация @EntityResult позволяет для таких ситуаций задать discriminatorColumn, но мне не удалось заставить его работать. Маппинг работал некорректно, значения пропертей маппились некорректно в любом варианте использования. Более того, выяснилось, что все столбцы должны иметь уникальный префикс. Иначе вложенные объекты могут захватить свойство (например, ID) внешнего объекта, чей столбец ID был без префикса. Всё это выглядит настолько бажным, что становится странным, как такое количество багов может считаться приемлемым.

Работающий маппинг теперь имел вид:

@SqlResultSetMapping( name = "request-department", entities = {
        @EntityResult( entityClass = Request.class, fields = {
                @FieldResult( name = "id", column = "R_ID"),
                @FieldResult( name = "deadlineDate", column = "R_DEADLINE_DATE"),
                @FieldResult( name = "correctedDeadlineDate", column = "R_CORRECTED_DEADLINE_DATE")
        }),
        @EntityResult( entityClass = Department.class, fields = {
                @FieldResult( name = "id", column = "DEP_ID"),
                @FieldResult( name = "description", column = "DEP_DESCRIPTION"),
                @FieldResult( name = "name", column = "DEP_NAME")
        }),
        @EntityResult( entityClass = User.class, fields = {
                @FieldResult( name = "id", column = "U_ID"),
                @FieldResult( name = "email", column = "U_EMAIL"),
                @FieldResult( name = "personalFirstName", column = "U_FIRST_NAME"),
                @FieldResult( name = "personalLastName", column = "U_LAST_NAME"),
                @FieldResult( name = "personalMiddleName", column = "U_MIDDLE_NAME")
        }),
        @EntityResult( entityClass = User.class, fields = {
                @FieldResult( name = "id", column = "ASS_ID"),
                @FieldResult( name = "email", column = "ASS_EMAIL"),
                @FieldResult( name = "personalFirstName", column = "ASS_FIRST_NAME"),
                @FieldResult( name = "personalLastName", column = "ASS_LAST_NAME"),
                @FieldResult( name = "personalMiddleName", column = "ASS_MIDDLE_NAME")
        })
})

Left join и вложенные объекты

Тут я вспомнил, что мне на самом деле нужен не INNER JOIN, а LEFT JOIN. То есть мне нужно сделать один из @EntityResult – nullable. Но этого сделать нельзя ! EclipseLink видит, что User.Id – первичный ключ, и падает на утверждении, что он не может быть Null. Ничего нельзя сделать, кроме того, что подставлять в столбец магическое число, сигнализирующее о том, что на самом деле сущность отсутствует. Вложенные объекты, кстати, тоже подцепить не удаётся. Но можно их зацепить отдельно, а потом вручную присвоить свойствам внешнего объекта.

http://www.eclipse.org/forums/index.php/t/305321/ https://www.java.net/node/675607

Параметры

Ок, после явного прописывания всех столбцов и использования -1 для сигнализации null-объектов, все работало. Теперь я подумал, что неплохо бы добавить в мой Named Native Query параметры. Как оказалось, это невозможно. Параметры в named native queries не работают вообще и никак. Не работает ничего: ни именование с двоеточия, ни вопросиками, ни через решётку (как предлагают в одном из ответов).

http://eclipse.1072660.n5.nabble.com/Params-on-NamedNativeQuery-td3401.html

Пришлось убрать аннотацию @NamedNativeQuery, и сделать всё руками:

Query nativeQuery = entityManager.createNativeQuery(
    String.format( sql, daysBeforeDeadline, RequestStatus.Sent.ordinal(), RequestStatus.Processing.ordinal() ),
    "request-department" );
List<Object[]> resultList = nativeQuery.getResultList();
 
for ( Object[] rowObjects : resultList ) {
    Request request = ( Request ) rowObjects[0];
    Department department = ( Department ) rowObjects[1];
    User user = ( User ) rowObjects[2];
    User assignee = (User) rowObjects[3];
    request.setDepartment( department );
    request.setUser( user );
    if (assignee.getId() != -1)
    request.setAssignee( assignee );
    requests.add( request );
}
return requests;

Прикол в том, что и здесь параметры тоже не работают ! Приходится вставлять их с помощью String.format().

(Тут вроде у мужиков работает, но у меня и так не заработало)

Заключение

В общем, я рад, что таки-получилось заставить это работать, но какой ценой ? Программистам EclipseLink стоит задуматься о качестве своего продукта, если на такие простые вещи за столько лет разработки у них не отработаны работающие сценарии на одной из самых популярных СУБД, тем более что они теперь – вендоры этой ORM. Но если вы желаете использовать в своём проекте native queries, то лучше придерживаться следующих правил:

  • Не использовать @NamedNativeQuery
  • Явно задавать все имена столбцы, они должны быть уникальны и не должны совпадать по названию ни с одним из свойств вложенных сущностей. Лучше всего задать уникальные префиксы для каждой сущности.
  • Там, где ожидаются nullable-столбцы, но которые мапятся на свойства, не допускающие значений null, придётся использовать магические константы или другие столбцы в качестве флагов
  • Параметры не работают – придётся вставлять их вручную

Внедряем данные из spring model в js-код

Written by elwood

Часто бывает нужно передать данные с серверной части на клиентскую, причём не в виде текста, а в виде готовых объектов. К сожалению, в HTML-страницу нельзя внедрить данные напрямую, но можно сгенерировать js-скрипт, в который бы передавался JSON, готовый к употреблению. Обычно я делал это вручную, но теперь решил написать спринговый interceptor, который бы перехватывал все атрибуты model, начинающиеся с “js_”, и добавлял их на клиент автоматически.

Пример использования

@RequestMapping(value = "/create", method = RequestMethod.GET)
public String viewCreate(Model model) {
	NewsArticle article = new NewsArticle();
        // Этот атрибут будет доступен только в JSP при генерации страницы
	model.addAttribute( "article", article );
	// А этот атрибут также попадёт и в js-код в качестве атрибута глобального объекта JS_DATA
	model.addAttribute( "js_article_id", article.getId() );
	return "edit";
}
$(function() {
    var articleId = JS_DATA['js_article_id'];
    // ...
});

Код

Все необходимые кусочки файлов можно посмотреть на github, а здесь приведу только код интерсептора:

public class JsDataInjectionInterceptor extends HandlerInterceptorAdapter
{
    private final static Log log = LogFactory.getLog( JsDataInjectionInterceptor.class );
 
    @Override
    public void postHandle( HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView ) throws Exception {
        //
        if (null == modelAndView) return;
 
        Map<String, Object> jsObjects = null;
        for ( Map.Entry<String, Object> entry : modelAndView.getModelMap().entrySet() ) {
            if (entry.getKey().startsWith( "js_" )) {
                if (null == jsObjects)
                    jsObjects = new HashMap<>(  );
                jsObjects.put( entry.getKey(), entry.getValue() );
            }
        }
        if (jsObjects != null) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable( SerializationFeature.INDENT_OUTPUT );
            String jsData = mapper.writeValueAsString( jsObjects );
            log.debug( "JsData: " + jsData );
            modelAndView.getModelMap().addAttribute( "__js_data__",
                    jsData
            );
        }
    }
}

Исследование Chrome Secure Shell

Written by elwood

Узнал о приложении Secure Shell – ssh-клиенте и эмуляторе терминала, работающем в броузере. Меня удивило, что эта штука, написанная на js, действительно работает – неужели протокол ssh с шифрованием действительно был реализован на javascript? Или он проксирует данные через внешний сервер, передавая туда сочетания клавиш, а обратно получая команды для эмулятора терминала в plain text ? При этой мысли по спине пробежал холодок. Я только что подключался к одному серверу и ввёл там правильный пароль. Если догадка верна, то мой пароль мог легко попасть к злоумышленникам. В общем, пришлось поковырять поглубже, чтобы выяснить, как всё работает.

После изучения FAQ, исходников и файлов, поставляемых с расширением, выяснено было следующее:

  • Это приложение написано в рамках проекта Chromium OS, и его исходники открыты.

  • Оно действительно написано на js, но реализации ssh в нём нет, но есть нативный плагин, встроенный в расширение при помощи технологии Native Client. В нём-то и лежит реализация ssh-протокола, а доступ к нему осуществляется через джаваскриптовое API, предоставляемое броузером Google Chrome.

  • Но механизм проксирования также присутствует ! Называется он Relay Server, и иногда работает. Из документации непонятно, при каких ситуациях используется именно этот режим. Написано что по умолчанию используется native client, но можно и специально заставить использовать relay, указав его опции в строке Relay options. Почему-то списка доступных настроек найти не удалось. По исходникам тоже непонятно, в каком случае используется native client, а в каком – relay server. Исходные коды гуглового relay сервера закрыты, в комментариях написано, что “Вы можете написать такой сервер сами, вам достаточно зареверсинжинирить такой-то js-файл”.

  • Есть джавский relay-сервер в открытых источниках: https://github.com/zyclonite/nassh-relay/
    Демосервер, на нём можно проверить работу https://relay.wsn.at/

    Заодно мы узнаём опции, которые нужно добавить в строку relay options:

    --proxy-host=relay.wsn.at --proxy-port=443 --use-ssl

    Решил проверить этот сервер, не вводя там правильного пароля – всё заработало, но строка Loading Native Client всё равно смущает. Попробовал удалить ssh_client_nl_x86_64.nexe, после этой операции Secure Shell не хотел подключаться, хотя опции для релея были.

  • Вот ещё один relay-server, здесь уже на пейтоне: https://github.com/raptium/hashi

    Выглядит очень лаконично, по сравнению с джавской реализацией. Заодно узнаём ещё один способ задания relay-server:

    To connect via the relay server, use USER@SSH_SERVER[:SSH_PORT]@RELAY_SERVER as the destination in the secure shell.

  • Native Client – очень интересная фигня. Файлы nexe, поставляемые с расширением, разные для разных процессорных платформ, но одинаковые для операционных систем. То есть ssh_client_nl_x86_64.nexe может успешно использоваться и на Windows, и на Linux, и на Mac OS X, главное чтобы система была 64-битная. Это обстоятельство меня смутило, т.к. проводя первичный осмотр в Total Commander, я обнаружил, что эти файлы начинаются с ELF, а значит, что это либо работает только на линуксах, либо где-то дальше в файле лежат нативные файлы в других форматах – PE, dylib. Такой типа самопальный fat executable. Но всё оказалось ещё интереснее. Выяснилось, что гугл в этом месте не хранит нативный готовый код, а какой-то байткод, получаемый на выходе nacl-toolchain. В сети упоминался байткод LLVM, но тут пока неясно, в чём отличие pexe от nexe. pexe не зависит от платформы, nexe-зависит. Интересно, чем они отличаются внутри. С этим пока разобраться не удалось.

    Дополнительный прикол заключаются в том, что эти бинарники безопасны, хотя и содержат нативный код. Дело в том, что они могут быть собраны только с теми библиотеками, которые портированы в nacl-ports. А верификатор броузера при загрузке этого кода проверяет, не содержит ли он потенциально небезопасных инструкций – к таким, например, относятся syscall и int.

Общий вывод

Secure Shell скорее всего безопасен для применения, но точно не ясно, при каких случаях может включаться механизм проксирующих серверов, который небезопасен по своей сути. При paranoid_mode = ON лучше либо не пользоваться этой тулзой, либо до конца разобраться в логике включения релеев.