среда, 25 сентября 2013 г.

ADF Mobile: Программная реализация вызова REST XML веб-сервиса, возвращающего данные в кодировке Cp1251

Экспериментируя недавно с вызовом REST служб в Oracle ADF Mobile приложении, я столкнулся с одной интересной особенностью: при попытке вызова REST XML веб-службы, возвращающей данные в кодировке Cp1251 при помощи артефакта REST DataControl возникает ошибка, что данная кодировка не поддерживается:

Invalid stream or encoding: java.io.UnsupportedEncodingException: Cp1251 (position:START_DOCUMENT null@0:0) caused by: java.io.UnsupportedEncodingException: Cp1251; severity: ERROR; .type: oracle.adfmf.framework.exception.AdfException; .exception: true; }

Эксперименты проводились с вызовом REST веб-сервиса ЦБ РФ, возвращающего информацию о курсах валют на текущую дату. URL Endpoint сервиса: http://www.cbr.ru/scripts/XML_daily.asp

Собственно, пытаясь найти решение данной проблемы, был сделан вывод, что для вызова подобных служб на данном этапе необходимо использовать программный подход и реализовывать всю логику вызова сервиса в Java коде, а не декларативно.
Рассмотрим решение этой проблемы по этапам:

Шаг 1. В среде JDeveloper создаем URL Connection (File -> New -> Connections -> URL Connection), где указываем URL Endpoint REST XML службы. В нашем случае это веб сервис ЦБ РФ с URL: http://www.cbr.ru/scripts/XML_daily.asp


Шаг 2. Для хранения данных, получаемых от веб-сервиса в ApplicationController проекте создаем дополнительный Data Object Java класс. Это вспомогательный класс, который позволяет хранить атрибутивные данные об объете (в нашем случае это курс валюты) и содержит в себе соответсвующие getter/setter методы. В нашем примере мы используем класс Currency, где определяем переменные, хранящие всю атрибутивную информацию о курсе валюты (код, наименование, значение и т.д.). Наполнение эти данных происходит в конструкторе при создании экземпляра этого класса.

public class Currency {
    private String NumCode;
    private String CharCode;
    private String Nominal;
    private String Name;
    private String Value;
    private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

    public Currency() {
        super();
    }
    
    public Currency(String NumCode, String CharCode, String Nominal, String Name, String Value) {
        super();
        this.NumCode = NumCode;
        this.CharCode = CharCode;
        this.Nominal = Nominal;
        this.Name = Name;
        this.Value = Value;
    }    
    
// ...
// Other setters and getters here
// ...

    public void setValue(String Value) {
        String oldValue = this.Value;
        this.Value = Value;
        propertyChangeSupport.firePropertyChange("Value", oldValue, Value);
    }

    public String getValue() {
        return Value;
    }
}

Шаг 3. В проекте ApplicationController необходимо также создать еще один дополнительный, так называемый Service Object Java класс, который позже будет использоваться для создания Data Control артефакта. В нем, предварительно определив get метод, необходимо реализовать программный вызов службы, используя класс RESTServiceAdapter. В нашем случае Service Object Java класс это класс CurrencyRatesService. 


RestServiceAdapter restServiceAdapter = Model.createRestServiceAdapter();

// Clear any previously set request properties, if any
restServiceAdapter.clearRequestProperties();


// Set the connection name
restServiceAdapter.setConnectionName("CBRF");

// Specify the type of request   restServiceAdapter.setRequestType(RestServiceAdapter.REQUEST_TYPE_GET);
restServiceAdapter.addRequestProperty("Content-Type", "application/xml");
restServiceAdapter.addRequestProperty("Accept", "application/xml; charset=windows-1251");

// Specify the number of retries
restServiceAdapter.setRetryLimit(0);

// Set the URI which is defined after the endpoint in the connections.xml.
// The request is the endpoint + the URI being set

restServiceAdapter.setRequestURI("");

Шаг 4. Данные, возвращаемые веб-сервисом необходимо получать в формате массива byte, для этого нужно использовать sendReceive метод класса restServiceAdapter;

byte[] response = null;
response = restServiceAdapter.sendReceive("");

Шаг 5.  После получения byte массива в кодировке Cp1251 необходимо преобразовать его в UTF-8 строку. Подробнее об алгоритме этого процесса можно ознакомиться здесь. В нашем случае преобразование реализовывается в static decodeCp1251 методе, определенном в этом же классе (CurrencyRatesService).

// Byte array into UTF8 string transformation.
String responseString = "";
responseString = decodeCp1251(response);

Шаг 6. На предыдущем этапе мы получили строку в формате UTF-8 содержащую XML данные от вызываемого веб-сервиса. На этом шаге необходимо выполнить парсинг полученного XML документа с помощью kXML библиотеки и наполнить данными структуру ArrayList. В нашем случае ArrayList будет содержать объекты класса Currency, определенного нами ранее:

// For GET request, there is no payload
response = restServiceAdapter.sendReceive("");

// Byte array into UTF8 string transformation.
responseString = decodeCp1251(response);

KXmlParser parser = new KXmlParser();
parser.setInput(new InputStreamReader(new ByteArrayInputStream(responseString.getBytes())));
            
parser.nextTag();            
parser.require(XmlPullParser.START_TAG, null, "ValCurs");
while(parser.nextTag() == XmlPullParser.START_TAG) {
  parser.require(XmlPullParser.START_TAG, null, "Valute");
  while(parser.nextTag() == XmlPullParser.START_TAG) {
    parser.require(XmlPullParser.START_TAG, null, "NumCode");
    // handle element content
    String NumCode = parser.nextText();
    parser.require(XmlPullParser.END_TAG, null, "NumCode");
                    
    parser.nextTag();                   
                    
    parser.require(XmlPullParser.START_TAG, null, "CharCode");
    
    // handle element content
    String CharCode = parser.nextText();
    parser.require(XmlPullParser.END_TAG, null, "CharCode");
                    
    parser.nextTag();
                    
    parser.require(XmlPullParser.START_TAG, null, "Nominal");
    
    // handle element content
    String Nominal = parser.nextText();
    parser.require(XmlPullParser.END_TAG, null, "Nominal");
                    
    parser.nextTag();
                    
    parser.require(XmlPullParser.START_TAG, null, "Name");
    
    // handle element content
    String Name = parser.nextText();  
    parser.require(XmlPullParser.END_TAG, null, "Name");
                    
    parser.nextTag();
                    
    parser.require(XmlPullParser.START_TAG, null, "Value");
    
    // handle element content
    String Value = parser.nextText();
    parser.require(XmlPullParser.END_TAG, null, "Value");
                    
    currencyList.add(new Currency(NumCode, CharCode, Nominal,         Name, Value));
                }
    parser.require(XmlPullParser.END_TAG, null, "Valute");
            }

parser.require(XmlPullParser.END_TAG, null, "ValCurs");
            

currency = (Currency[]) currencyList.toArray(new Currency[currencyList.size()]);


Подробнее о работе с kXML библитекой можно прочитать в документации:

Шаг 7. После преобразования данных в UTF-8 кодировку и наполнения структуры ArrayList необходимо привести их к виду, удобному для использования в UI слое Web View, в AMX страницах. Для этого необходимо преобразовать их в артефакт Data Control. В окне Application Navigator необходимо выделить Service Object Java класс (в нашем случае CurrencyRatesService), и вызвав контекстное меню, выбрать пункт Create Data Control.



После этого в окне Data Controls появится вновь созданный data control артефакт с коллекцией данных внутри него (в нашем случае это CurrencyRatesService data control с коллекцией данных currencyRates).



Шаг 8. Выделите коллекцию данных в окне Data Controls и перетащите её методом Drag&Drop на вновь созданную AMX страницу как List View AMX компонент.


Шаг 9. Приложение готово. Создайте профиль развертывания (Deployment Profile) и протестируйте его на эмуляторе или на реальном устройстве (Android, IOS).

В качестве дополнительной опции, при создании List View компонента, я использовал Main-Sub формат списка, т.е. элемент списка состоит из 2 частей: главной подписи (наименование валюты) и дополнительной подписи (значение курса). Все элементы списка группируются по алфавиту. Для этого, при создании List View в окне Edit List View значение Divider Mode необходимо задать как First Letter.


И, наконец, для большей функциональности приложения был создан taskFlow артефакт с 2-мя страницами: списком курсов валют и детальной информацией по каждому курсу:




Демонстрационный проект приложения можно загрузить здесь


Комментариев нет:

Отправить комментарий