Довольно часто в жизни разработчиков встречаются ситуации «когда очень хочется, но нельзя». И очень часто этот вопрос решается, как все в нашей стране — «если очень хочется, то можно». Сегодня я хочу рассказать вам про мой опыт создания независимого API для web-проекта, который этот API не предоставляет. Статья будет полезна Java или Solaris разработчикам, а так же всем тем кто сталкивается с проблемой интеграции различных сервисов.
Не так давно для всех разработчиков был открыт проект SourceJuicer (http://jucr.opensolaris.org/), который позволяет публиковать свои проекты для OpenSolaris, проводить сборку и публиковать их в открытые репозитории. Описание проекта делается в виде spec-файла (http://jucr.opensolaris.org/help/spec_file), в котором перечислены атрибуты проекта, как его собирать, откуда скачивается исходный код, под какими лицензиями он распространяется и т.п. Все необходимые для сборки файлы загружаются на сервер и после review он будет собран и выложен в репозиторий. Вроде бы все классно, но в чем подвох? А подвох в том, что создавать проект и загружать или обновлять файлы можно только через web интерфейс. Что в принципе не так смертельно для небольшого проекта, но уже при 10-ти файлах этот процесс загрузки-обновления начинает несколько утомлять.
Кроме того такая система загрузки абсолютно не подходит для Continuous Integration — для чего в сущности и создавался SourceJuicer. Поэтому дальше я расскажу как использовать этот сервис (или любой другой) максимально эффективно и может для кого-то, это подкинет интересные идеи на будущее.
С чего мы начнем? Мастера всегда узнают по инструменту, а человека, который исследует чужой web-сайт, по Firebug. Как правило сценарий работы пользователя с какой-либо системой выглядит так
Для того чтобы сделать свое web API для сайта, нам необходимо максимально точно описать эти действия пользователя и выполнять их по необходимости. Например, авторизация это не что иное, как получение правильных cookies в ответ на форму. Поэтому берем Firefox с установленным Firebug (http://getfirebug.com/) и Firecookie (http://www.softwareishard.com/blog/firecookie/), заходим на страницу авторизации и смотрим, какие cookie были проставлены при удачной авторизации.
Как правило при авторизации выдается метка-«номер сессий» (token), которая не меняется при запросах. Это может быть «jsessionid», «phpsession», «OSS» и т.п. Но возможны и варианты с изменением cookie-метки при повторных запросах, так что это стоит учитывать. В случае авторизации на SourceJuicer используются три cookie, так что все три придется сохранять и передавать при запросах. Так же не стоит забывать, что страница с формой авторизации не всегда является начальной точкой путешествия, часто для того чтобы инициализировать сессию необходимо зайти на стартовую страницу сервера или пользователя.
Мы знаем что нам нужно сделать, так что теперь посмотрим как это описать в коде. Есть такая замечательная библиотека HttpUnit (http://httpunit.sourceforge.net/), которая позволяет проводить тестирование web-интерфейса — переходить по ссылкам, отправлять формы, проверять содержимое таблиц и много-много всего интересного. Но сейчас она нам интересна как простой способ отправлять http-запросы и обрабатывать ответы. Для того чтобы использовать эту библиотеку необходимо подключить html-парсер и интерпретатор javascript. Я использовал nekohtml (http://nekohtml.sourceforge.net/) и Mozilla rhino (http://www.mozilla.org/rhino/) — хотя neko не рекомендовал бы для production из-за жесткой зависимости от xerces.
Итак нам надо зайти на страничку, получить cookies и отправить форму. Код для этого выглядит так
Следующий вариант, когда мы обновляем проект на SourceJuicer не все файлы уже могут быть необходимы для сборки проекта. Поэтому нам надо проверить какие файлы устарели и удалить их. Через web-интерфейс это решается следующим образом — открывается страница существующего проекта, в первой таблице есть список файлов и напротив каждого файла кнопка «Delete». При нажатии на кнопку появляется форма с подтверждением и после нажатия на «Confirm» файл удаляется.
Как все это выглядит в коде
Так что с помощью нехитрых манипуляций с cookies, запросов и парсинга html, мы получили рабочий вариант api (http://bitbucket.org/abashev/pusher/src/ade4e90f01a9/src/main/java/org/bitbucket/pusher/api/SourceJuicerAPI.java), который может использоваться так как удобно нам, а не разработчику сервиса.
Полная версия проекта лежит вот тут — bitbucket.org/abashev/pusher
С удовольствием выслушаю ваши вопросы и пожелания.
Не так давно для всех разработчиков был открыт проект SourceJuicer (http://jucr.opensolaris.org/), который позволяет публиковать свои проекты для OpenSolaris, проводить сборку и публиковать их в открытые репозитории. Описание проекта делается в виде spec-файла (http://jucr.opensolaris.org/help/spec_file), в котором перечислены атрибуты проекта, как его собирать, откуда скачивается исходный код, под какими лицензиями он распространяется и т.п. Все необходимые для сборки файлы загружаются на сервер и после review он будет собран и выложен в репозиторий. Вроде бы все классно, но в чем подвох? А подвох в том, что создавать проект и загружать или обновлять файлы можно только через web интерфейс. Что в принципе не так смертельно для небольшого проекта, но уже при 10-ти файлах этот процесс загрузки-обновления начинает несколько утомлять.
Кроме того такая система загрузки абсолютно не подходит для Continuous Integration — для чего в сущности и создавался SourceJuicer. Поэтому дальше я расскажу как использовать этот сервис (или любой другой) максимально эффективно и может для кого-то, это подкинет интересные идеи на будущее.
С чего мы начнем? Мастера всегда узнают по инструменту, а человека, который исследует чужой web-сайт, по Firebug. Как правило сценарий работы пользователя с какой-либо системой выглядит так
- Выполняем авторизацию.
- Заходим по нужной ссылке.
- Заполняем поля формы.
- Подтверждаем действие.
- С пункта 2 повторить по необходимости.
Для того чтобы сделать свое web API для сайта, нам необходимо максимально точно описать эти действия пользователя и выполнять их по необходимости. Например, авторизация это не что иное, как получение правильных cookies в ответ на форму. Поэтому берем Firefox с установленным Firebug (http://getfirebug.com/) и Firecookie (http://www.softwareishard.com/blog/firecookie/), заходим на страницу авторизации и смотрим, какие cookie были проставлены при удачной авторизации.
Как правило при авторизации выдается метка-«номер сессий» (token), которая не меняется при запросах. Это может быть «jsessionid», «phpsession», «OSS» и т.п. Но возможны и варианты с изменением cookie-метки при повторных запросах, так что это стоит учитывать. В случае авторизации на SourceJuicer используются три cookie, так что все три придется сохранять и передавать при запросах. Так же не стоит забывать, что страница с формой авторизации не всегда является начальной точкой путешествия, часто для того чтобы инициализировать сессию необходимо зайти на стартовую страницу сервера или пользователя.
Мы знаем что нам нужно сделать, так что теперь посмотрим как это описать в коде. Есть такая замечательная библиотека HttpUnit (http://httpunit.sourceforge.net/), которая позволяет проводить тестирование web-интерфейса — переходить по ссылкам, отправлять формы, проверять содержимое таблиц и много-много всего интересного. Но сейчас она нам интересна как простой способ отправлять http-запросы и обрабатывать ответы. Для того чтобы использовать эту библиотеку необходимо подключить html-парсер и интерпретатор javascript. Я использовал nekohtml (http://nekohtml.sourceforge.net/) и Mozilla rhino (http://www.mozilla.org/rhino/) — хотя neko не рекомендовал бы для production из-за жесткой зависимости от xerces.
Итак нам надо зайти на страничку, получить cookies и отправить форму. Код для этого выглядит так
wc.getResponse(JUICER_HOME_URL); // Creating new jsession
wc.getResponse(JUICER_AUTH_URL); // Go to login
WebResponse response = wc.getResponse(AUTH_URL); // Load login form
WebForm loginForm = null;
for (WebForm form : response.getForms()) {
if (form.getAction().equals("/login.action")) {
loginForm = form;
break;
}
}
loginForm.setParameter("userName", username);
loginForm.setParameter("password", password);
loginResult = loginForm.submit(); // Submit user name and password
* This source code was highlighted with Source Code Highlighter.
Следующий вариант, когда мы обновляем проект на SourceJuicer не все файлы уже могут быть необходимы для сборки проекта. Поэтому нам надо проверить какие файлы устарели и удалить их. Через web-интерфейс это решается следующим образом — открывается страница существующего проекта, в первой таблице есть список файлов и напротив каждого файла кнопка «Delete». При нажатии на кнопку появляется форма с подтверждением и после нажатия на «Confirm» файл удаляется.
Как все это выглядит в коде
WebResponse response = conversation.getResponse(String.format(JUICER_SUBMISSION_MASK, submissionId));
WebTable fileList = response.getTableStartingWith("Summary"). // Ищем таблицу с заголовком Summary
getTableCell(2, 1). // Берем ячейку из третьего ряда во второй колонке
getTables()[0]; // А в ней еще одна таблица
Collection<String> forDelete = new ArrayList<String>();
for (int i = 0; i < fileList.getRowCount(); i++) {
WebLink link = fileList.getTableCell(i, 0).getLinks()[0]; // Находим ссылку на файл
String fullName = link.getText().replaceAll("\\s+", "");
String fileName = fullName.replaceAll("[^/]+/([^/]+)", "$1");
SubmitType type = detectType(fullName);
if (existFileInSubmission(files, type, fileName) == null) {
String fileId = fileList.getTableCell(i, 1).getForms()[0].getParameterValue("file_id"); // Выдергиваем id файла для удаления
forDelete.add(fileId);
debug("Need to delete file [%s]", fullName);
}
}
for (String fileId : forDelete) {
PostMethodWebRequest post = new PostMethodWebRequest(
String.format(JUICER_DELETE_MASK, submissionId),
true
);
post.setParameter("delete_file", "Delete File");
post.setParameter("file_id", fileId);
WebResponse confirmResponse = conversation.getResponse(post); // Отправляем форму для удаления
WebForm form = confirmResponse.getForms()[0];
SubmitButton confirm = form.getSubmitButtons()[1]; // Находим кнопку Confirm ...
WebResponse r = form.submit(confirm); // ... и нажимаем ее
}
* This source code was highlighted with Source Code Highlighter.
Так что с помощью нехитрых манипуляций с cookies, запросов и парсинга html, мы получили рабочий вариант api (http://bitbucket.org/abashev/pusher/src/ade4e90f01a9/src/main/java/org/bitbucket/pusher/api/SourceJuicerAPI.java), который может использоваться так как удобно нам, а не разработчику сервиса.
Полная версия проекта лежит вот тут — bitbucket.org/abashev/pusher
С удовольствием выслушаю ваши вопросы и пожелания.