В этой статье рассказывается о работе с API XML / HTTP из Groovy в контексте реального сценария, использующего API GoogleCode.
Эта статья впервые появилась в мартовском выпуске GroovyMag за 2013 год . Поскольку сценарий был изначально написан, Google отказался от API отслеживания ошибок и запланировал его закрытие 14 июня 2013 года. Поэтому, хотя сценарий теперь предназначен только для интереса, принципы по-прежнему действительны для других целей.
Некоторое время назад у меня было требование переименовать проект Google Code — это было связано с опечаткой в названии проекта, а не с уведомлением о прекращении действия. Как только мой коллега назначил мне разрешения на уровне владельца, я обнаружил, что даже расширенные возможности администрирования не позволяют вам изменять имя проекта. Однако, поскольку у Google есть проблемы, у них есть API, и, будучи непростым участником Groovy, было непросто перенести проблемы в новый проект (sans-typo).
Это было на раннем этапе жизненного цикла проекта, поэтому основной проблемой была не миграция кода (есть несколько постов в блоге с инструкциями по этому вопросу в зависимости от вашего вида контроля исходного кода) — скорее это было множество вопросов и комментариев от мастерская по сбору требований.
Окончательный скрипт доступен на GitHub в виде гистограммы: https://gist.github.com/rbramley/5073413
API и набор инструментов
API отслеживания проблем задокументирован по адресу http://code.google.com/p/support/wiki/IssueTrackerAPI.
Это RESTful API, использующий фиды / записи Atom, поэтому мы будем использовать следующие инструменты:
- Apache HttpComponents HttpClient 4.x (заменено commons-httpclient 3.x)
- XmlSlurper — для разбора Atom Feeds, которые мы получаем
- MarkupBuilder — для создания записей Atom в POST
ЗАВИСИМОСТИ
HttpClient и его зависимости можно получить с помощью @Grab
аннотаций Groovy GRAPE, показанных в листинге 1. GRAPE (GRoovy Advanced Packaging Engine — http://groovy.codehaus.org/Grape ) использует Apache Ivy для разрешения зависимостей и был представлен в Groovy 1.6.
@Grab(group='commons-logging', module='commons-logging', version='1.1.1') @Grab(group='commons-codec', module='commons-codec', version='1.4') @Grab(group='org.apache.httpcomponents', module='httpclient', version='4.1.2')
Листинг 1 : Схватить аннотации
Как работает скрипт?
Чтобы лучше понять контекст примеров, полезно быстро вспомнить, что делает сценарий — процесс выглядит следующим образом:
- Форма POST учетные данные
- Если не запрещено 403, извлеките токен авторизации (для использования в заголовке авторизации для всех последующих запросов)
- ПОЛУЧИТЬ список проблем из исходного проекта
- Разобрать тело ответа Atom XML (объявив пространство имен проблем http://schemas.google.com/projecthosting/issues/2009 )
- Для каждой записи в ленте:
- Извлеките идентификатор проблемы
- Преобразовать запись в требуемую форму
- POST запись Atom в URL создания проблемы целевого проекта
- Если не 400 плохих запросов, ПОЛУЧИТЕ список комментариев по идентификатору проблемы из исходного проекта
- Разобрать тело ответа Atom XML (объявляя пространство имен проблем как и прежде)
- Для каждой записи в ленте:
- Преобразовать запись в требуемую форму
- POST запись Atom в URL создания комментария целевого проекта для текущей проблемы
Основы HTTP-взаимодействия
Для работы с API требуется использование двух глаголов, GET и POST. Примеры опираются на определение HttpClient httpclient = new DefaultHttpClient()
ПОЛУЧИТЬ
get = new HttpGet(issuesCommentsListUrl) get.setHeader('Authorization', "GoogleLogin auth=${authToken}") response = httpclient.execute(get) println "${response.getStatusLine().getStatusCode()} - ${response.getStatusLine().getReasonPhrase()}" commentsAtom = EntityUtils.toString(response.entity)
Листинг 2 : Метод HttpClient GET
Комментарий к листингу 2 состоит из двух частей: создание запроса и обработка ответа.
Для запроса мы должны создать объект HttpGet с целевым URL, добавить токен авторизации в заголовок и затем дать указание HttpClient выполнить наш запрос.
С точки зрения обработки ответа, мы требуем его как строку для синтаксического анализа XML, поэтому объект должен быть получен и преобразован в строку.
Хотя это довольно просто, он не превосходит простоту расширенного getText
метода Groovy в классе java.net.URL (например def response = new URL(url).getText()
) — в этом случае HttpClient используется для согласованности и для возможности установки заголовков.
LOGIN FORM POST
Для первой операции POST требуется регистрация формы для входа в систему, и, как показано в листинге 3, параметры формы создаются с использованием NameValuePair и добавляются в список. Этот список используется для создания объекта формы в кодировке URL, установленного для объекта HttpPost .
// set up login parameters NameValuePair accountType = new BasicNameValuePair('accountType', 'GOOGLE') NameValuePair email = new BasicNameValuePair('Email', emailAddress) NameValuePair passwd = new BasicNameValuePair('Passwd', password) NameValuePair service = new BasicNameValuePair('service', 'code') NameValuePair source = new BasicNameValuePair('source', sourceScript) List params = new ArrayList(5) params.addAll() HttpPost post = new HttpPost(googleLoginUrl) post.setEntity(new UrlEncodedFormEntity(params)) HttpResponse response = httpclient.execute(post)
Листинг 3 : Форма POST
Если код состояния ответа не 403 Запрещено, полученный токен авторизации извлекается из тела ответа.
ПОСТИНГ XML
Как видно из листинга 4, операции POST с полезной нагрузкой XML проще, поскольку мы отправляем XML с использованием StringEntity . В этом случае мы также должны установить Content-type
заголовок application/atom+xml
.
// post the issue post = new HttpPost(issuePostUrl) post.setHeader('Content-type', 'application/atom+xml') post.setHeader('Authorization', "GoogleLogin auth=${authToken}") post.setEntity(new StringEntity(issueCreationXml)) response = httpclient.execute(post)
Листинг 4 : POST тела строки
Groovy обработка XML
В Groovy есть несколько очень полезных вспомогательных классов для работы с XML — в следующих разделах будут рассмотрены XmlSlurper и MarkupBuilder, используемые сценарием. GPath, мощный набор функций для запроса вложенных структур данных, таких как XML-документ, выходит за рамки этой статьи, но заслуживает дальнейшего изучения, если вам нужно выполнить более сложную обработку XML, чем требуется для упражнения по отображению в этом сценарии.
ПОТРЕБЛЕНИЕ С XMLSLURPER
Groovy XML Slurper ([a href = «http://groovy.codehaus.org/Reading+XML+using+Groovy%E2%80%99s+XmlSlurper» rel = «nofollow» style = «font-style: inherit; color: rgb (51, 51, 51); «] http://groovy.codehaus.org/Reading+XML+using+Groovy’s+XmlSlurper) может анализировать XML в дереве объектов и поддерживает пространство имен. В листинге 5 приведен обзор важных полей в ленте Atom, которые используются в листингах 6 и 7. Поскольку этот сценарий был разработан для переноса проблем, мы разбираем XML на вложенные объекты (листинг 6), которые используются для генерации XML для воссоздание проблемы в целевом проекте (листинг 7).
<feed xmlns='http://www.w3.org/2005/Atom' ...> <entry> <id> <title> <content type='html'> <author> <name> </author> <issues:id> <issues:label>Type-Defect</issues:label> <issues:label>Priority-Medium</issues:label> <issues:owner> <issues:username> </issues:owner> <issues:state> <issues:status> </entry>
Листинг 5 : Поля ввода Atom
// Parse and process the atom feed feed = new XmlSlurper().parseText(atom).declareNamespace([issues:issuesXmlns]) feed.entry.each { entry -> issueId = entry.'issues:id' println "Issue ${issueId} - ${entry.title}" issueCreationXml = buildIssue(entry, issuesXmlns, atomXmlns)
Листинг 6 : XmlSlurper
ПРОИЗВОДСТВО С MARKUPBUILDER
MarkupBuilder — это вспомогательный класс для создания разметки XML или HTML с использованием синтаксиса построителя на основе замыканий. В отличие от XmlSlurper groovy.xml.MarkupBuilder не входит в стандартный путь к классам Groovy, поэтому его необходимо импортировать. В листинге 7 показано создание записи Atom с расширениями API проблем данных Google.
Для детального контроля над конкретными элементами вы можете использовать класс MarkupBuilderHelper ( http://groovy.codehaus.org/api/groovy/xml/MarkupBuilderHelper.html ), к которому осуществляется доступ mkp
и который позволяет напрямую вставлять экранированные или неэкранированные данные, например, b { mkp.yield( '3 < 5' ) }
какие результаты на выходе 3 <5 .
/** Build an atom feed for an issue */ def buildIssue(entry, issuesXmlns, atomXmlns) { def writer = new StringWriter() def xml = new MarkupBuilder(writer) xml.'atom:entry'('xmlns:atom':atomXmlns,'xmlns:issues':issuesXmlns) { 'atom:title'(entry.title) 'atom:content'(type:'html', entry.content) 'atom:author' { 'atom:name'(entry.author.name) } entry.'issues:label'.each { 'issues:label'(it) } 'issues:owner' { 'issues:username'(entry.'issues:owner'.'issues:username') } 'issues:state'(entry.'issues:state') 'issues:status'(entry.'issues:status') } return writer.toString() }
Листинг 7 : MarkupBuilder
GROOVY РУКОВОДСТВО ПОЛЬЗОВАТЕЛЯ
http://groovy.codehaus.org/GPath
http://groovy.codehaus.org/Processing+XML
http://groovy.codehaus.org/Reading+XML+using+Groovy%27s+XmlSlurper
http://groovy.codehaus.org/Creating+XML+using+Groovy%27s+MarkupBuilder
Executing the script
If you have the need to migrate your issues then the necessary steps are:
- Create your new target project
- Add the members from the old project (I did this manually) – this is critical to prevent creation errors
- Grab the IssueMigrator.groovy script from GitHub gist
- Change the project names, credentials and script source (lines 32-36)
- If you have more than 50 issues you’ll need to increase the max-results parameter on line 50
- Understand that the issues/comments will be created as the account running the script (and it has no warranty) – the original owners will be retained
- Run the script (either cross your fingers or run against a guinea pig project first!)
- Sit back and enjoy your new project
You may see some 400 ‘bad request’ responses being output from issue comments where it thinks there have been no updates. This can be ignored as the issue was created in the end state, rather than being created in the start state and then rolled forwards.