Знакомство с XPath на примере Google Geocoding API

Знакомство с XPath на примере Google Geocoding API

Один из последних солнечных и приятных дней октября (а вместе с тем и уходящего лета) подарил мне… новый баг, которым был крайне озадачен заказчик. В его системе не срабатывала кнопка «Определить координаты по адресу». Работала и вдруг перестала работать. И, конечно же, именно эта кнопка была самой важной и критичной именно в этот период.

Взглянув на код, унаследованный от предыдущего подрядчика и написанный году этак в 2008-м, увидел запросы к Google по адресу http://maps.google.com/maps/geo. Попробовал скомпоновать примерный URL, как это сделало бы приложение, и получил 403 Forbidden в ответ. Очевидно, проблема была не в нашем софте, а в запросе к сервису.

Немного поковырявшись в том же Google, нашел вот ссылку, объяснившую причину отказа системы. Оказалось, в нашем ПО используется версия 2 API геокодирования, которая была объявлена устаревшей 8 марта 2010, а поддерживаться перестала 8 марта 2013. Почему до сих пор у заказчика никто внимания на «сломанную» кнопку не обратил, конечно, остается большим вопросом (очевидно, настолько критична эта кнопка). В любом случае, нынче используется и поддерживается только третья версия API геокодирования.

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

<GeocodeResponse>
 <status>OK</status>
 <result>
  <type>street_address</type>
  <formatted_address>1600 Amphitheatre ... CA 94043, USA</formatted_address>
  <address_component>
   <long_name>1600</long_name>
   <short_name>1600</short_name>
   <type>street_number</type>
  </address_component>
  <address_component>
   <long_name>Amphitheatre Pkwy</long_name>
   <short_name>Amphitheatre Pkwy</short_name>
   <type>route</type>
  </address_component>
  <address_component>
   <long_name>Mountain View</long_name>
   <short_name>Mountain View</short_name>
   <type>locality</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>San Jose</long_name>
   <short_name>San Jose</short_name>
   <type>administrative_area_level_3</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>Santa Clara</long_name>
   <short_name>Santa Clara</short_name>
   <type>administrative_area_level_2</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>California</long_name>
   <short_name>CA</short_name>
   <type>administrative_area_level_1</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>United States</long_name>
   <short_name>US</short_name>
   <type>country</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>94043</long_name>
   <short_name>94043</short_name>
   <type>postal_code</type>
  </address_component>
  <geometry>
   <location>
    <lat>37.4217550</lat>
    <lng>-122.0846330</lng>
   </location>
   <location_type>ROOFTOP</location_type>
   <viewport>
    <southwest>
     <lat>37.4188514</lat>
     <lng>-122.0874526</lng>
    </southwest>
    <northeast>
     <lat>37.4251466</lat>
     <lng>-122.0811574</lng>
    </northeast>
   </viewport>
  </geometry>
 </result>
</GeocodeResponse>

Т.е. для того, чтобы узнать название улицы или почтовый индекс, теперь нужно пробегать по всем тэгам address_component и проверять их type.

Огромное нежелание писать такой парсер и мучаться с поиском ошибок привели к использованию XPath, о котором я уже слышал и даже разок использовал, что называется, straight forward. Теперь же XPath позволил упростить написание парсера ответа. Разберемся в механизме.

Для начала инициализируем XPath:

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

Затем из полученного ответа читаем XML документ в виде корневого узла.

InputSource xmlSource = processGoogleMapsRequest(address);
Node root = (Node) xpath.evaluate("/", xmlSource, XPathConstants.NODE);

Дальше будем работать с этим самым корнем root, который теперь представляет весь полученный от сервиса ответ. Отмечу, что вызов xpath.evaluate(…) с xmlSource закрывает этот самый InputSource, поэтому работа с объектом типа Node полезна и даже необходима, чтобы не обращаться к Geocoding сервису каждый раз, когда нам нужно узнать значение какого-нибудь атрибута.

Теперь у нас есть ответ от сервера в виде объекта Node. Переходим к самому интересному, а именно к целенаправленному точечному обращению к значению определенного тэга в интересующей нас группе тэгов. Например, мы хотим прочитать почтовый индекс. Для этого нам нужно найти тэг address_component, содержащий тэг type со значением postal_code. Нам осталось только указать путь к нужному значению с нужными условиями. В нашем случае применительно к приведенной выше XML структуре путь этот будет выглядеть так:

private static final String XPATH_ZIP_CODE = 
   "/GeocodeResponse/result/address_component[type/text() = 'postal_code']/long_name";

Итак, мы ищем тэг address_component, в квадратных скобках указываем условия поиска. В данном случае дочерний тэг type (один из тэгов) должен содержать текст ‘postal_code‘. Далее в найденном объекте address_component читаем содержимое тэга long_name.

Непосредственное считывание происходит примерно так:

Node zipCodeNode = (Node) xpath.evaluate(XPATH_ZIP_CODE, root, XPathConstants.NODE);
zipCodeNode.getTextContent();

Вот мы и считали почтовый индекс.

Признаюсь, про XPath слышал очень давно и ровно так же давно игнорировал его. Столкнувшись с конкретной задачей и оценив быстроту решения этой задачи, вынужден признать — иногда стоит все-таки смотреть и на те технологии, которые не вызывают особой симпатии. Теперь XPath будет, думаю, почаще встречаться в моих приложениях.