About

22 янв. 2012 г.

How to : JAX-WS + android / java me


Досталась однажды задачка написать приложение для телефонов. И не просто приложение , а коннектящееся к серверу для обмена данными,и с локальной бд. И конечно для начала нужно было выбрать на чем и как все это будет писаться. Предложенное RMI вскоре было отвергнуто по причинам весьма малого количества информации и примеров связки rmi + mobile , и то довольно старыми. Книг по rmi тоже не густо, одна изд. О'релли 2001г. Веб сервисы производили куда более благоприятное впечатление.
Далее из веб сервисов soap/rest был выбран soap , исключительно из-за простоты , ведь в приоритетах традиционно стоит скорость разработки. На стороне сервера все элементарно - веб сервис описывается аннотациями и уже готов к работе. На стороне клиента использую библиотеку для работы с соап: ksoap2.

Задача: написать приложение для мобильных устройств на ос андроид, а также мидлет для простых телефонов. Обмен данными с сервером реализовать веб сервисом soap. По веб сервису необходимо передавить не только простейшие типы, но и свой класс, список своих классов, класс содержащий простые типы и список своих классов.
среда разработки : Netbeans 7.0.1 ;
библиотеки : ksoap2-android , ksoap2-j2me-core-2.1.1
ос : Ubuntu 11.10 ;
сервер : Glassfish 3.1 ;

Процесс.
Вкрации :
1. Сервер.
2. Клиент андроид.
3. Классы передаваемые сервером.
4. Обработка принятых классов на клиенте.
5. Передача веб сервису сложных объектов.
5.1 Клиентсвие классы.
5.2 Серверные классы.
6. Клиент java me : незначительные изменения в коде.

1. Сервер.
Код серверной части самого сервиса состоит из интерфейса и реализации описанных аннотациями.
Интерфейс :
package org.setupit.ischool.webservices.for_mobile;
/**
 *
 * @author str
 */
import org.setupit.ischool.webservices.for_mobile.stubs.receive.RezultComplexStub;
import org.setupit.ischool.webservices.for_mobile.stubs.send.TestStub;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;

@WebService(name = "HelloWS", targetNamespace = "http://for_mobile/")
public interface IMobileLearningWS 
{
    @WebMethod(operationName = "login")
    public Integer login(@WebParam(name = "login") String login, @WebParam(name = "password") String password);
        
    @WebMethod(operationName="tests")
    @WebResult(name="TestStub")
    public TestStub[] tests(@WebParam(name = "userId") int userId);
            
    @WebMethod(operationName="receiveRezults")  
    public int receiveRezults(@WebParam(name=RezultComplexStub.NAMEELEMENT) RezultComplexStub task);      
} 
Реализация :
/**
 *
 * @author str
 */
@WebService( portName = "MobileLearningWSPort", 
        serviceName = "MobileLearningWSService", 
 targetNamespace = "http://for_mobile/", 
 endpointInterface = "org.setupit.ischool.webservices.for_mobile.IMobileLearningWS")
@LocalBean
public class MobileLearningWS implements IMobileLearningWS {
        
    @EJB
    UserFacade userFacade;

    @Override
    public Integer login(String login, String password) {      
      // ...
    }

    @Override
    public TestStub[] tests(int userId) {                
       // ...
    }

    @Override
    public int receiveRezults(RezultComplexStub rezultComplexStub) {                   
       // ...
    }        
}
В сервисе три метода, первый обрабатывает простые типы, второй возвращает список собственных классов, третий принимает сложный объект и возвращает код операции.
В нетбинсе автоматически появляется написанный сервис в списке веб сервисов.

2. Клиент андроид.
Добавить библиотеку ksoap2-android в папку libs, или через свойства проекта указать ее нахождение. Предпочтительней первый способ:

И в файле AndroidManifest.xml разрешить доступ к сети :
<uses-permission android:name="android.permission.INTERNET"></uses-permission>



Теперь можно начинать работать с этой библиотекой. Код клиента :

public class WSClient {

    static String DOMAIN_RELEASE = "*********";
    static String DOMAIN_DEVELOP = "127.0.0.1:80/";
    public static final String NAMESPACE = "http://for_mobile/";
    public static String URL = "http://"+DOMAIN_RELEASE+"/MobileLearningWSService?WSDL";
    public static final String METHOD_NAME_LOGIN = "login";
    public static final String METHOD_NAME_TESTS = "tests";
    public static final String METHOD_RECEIVE_REZ = "receiveRezults";
    public static final String SOAP_ACTION_LOGIN = NAMESPACE + METHOD_NAME_LOGIN;
    public static final String SOAP_ACTION_TESTS = NAMESPACE + METHOD_NAME_TESTS;
    public static final String SOAP_ACTION_RECEIVE_REZ = NAMESPACE + METHOD_RECEIVE_REZ;

    public UserModel login(String login, String passw) {
        SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME_LOGIN);

        PropertyInfo propertyLogin = new PropertyInfo();
        propertyLogin.name = "login";
        propertyLogin.type = PropertyInfo.STRING_CLASS;
        propertyLogin.setValue(login);

        PropertyInfo propertyPassw = new PropertyInfo();
        propertyPassw.name = "password";
        propertyPassw.type = PropertyInfo.STRING_CLASS;
        propertyPassw.setValue(passw);

        request.addProperty(propertyLogin);
        request.addProperty(propertyPassw);

         //
         // The constant SoapEnvelope.VER11 indicates SOAP Version 1.1. Assign the SoapObject 
         // request object to the envelop as the outbound message for the SOAP method call.
         //
        SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);

        envelope.setOutputSoapObject(request);

        // Create a org.ksoap2.transport.HttpTransportSE object that represents 
        // a J2SE based HttpTransport layer. HttpTransportSE extends the 
        // org.ksoap2.transport.Transport class, which encapsulates the serialization 
         // and deserialization of SOAP messages.         
        HttpTransportSE androidHttpTransport = new HttpTransportSE(URL);

        try {
            //  Make the soap call using the SOAP_ACTION and the soap envelop.
            //
            androidHttpTransport.call(SOAP_ACTION_LOGIN, envelope);
            
            // Get the web service response using the getResponse method of 
            // the SoapSerializationEnvelope object and cast the response object 
            // to SoapPrimitive, class used to encapsulate primitive types.
            //
            SoapPrimitive resultsRequestSOAP = (SoapPrimitive) envelope.getResponse();

            if (resultsRequestSOAP == null) {
                return null;
            }

            Integer userID = Integer.valueOf(resultsRequestSOAP.toString());
            return new UserModel(userID, login, passw);

        } catch (Exception e) {
            return null;
        }
    }

   public List<Testmodel> getTests(int userId) {

          // формирование параметров и запрос к серверу аналогично первой функции

            Object responceSoapObj = evenlope.getResponse();
            if (responceSoapObj == null) 
            {
               // ничего не принято

            } else if (responceSoapObj instanceof Vector) // принят список
            {
                Vector<SoapObject> soapOjs = (Vector<SoapObject>) responceSoapObj;               
                // ...
              
            } else // принят один объект
            {
                SoapObject so = (SoapObject) responceSoapObj;
                // ...
            }
         // ...
   }

    public String saveRezult(int learnerTestId, RezultFinalModel rezultFinal) {                
               
        // передача сложного объекта веб сервису, об этом в п.5      
    }

3. Классы передаваемые сервером.

Достаточно описать аннотациями класс, и он уже готов к передаче по сервису. Воспользуюсь спецификацией JAXB.
Если класс имеет конструктор, у него должен быть конструктор по умолчанию, указанный явно. Поля должны быть protected. Содержит другой объект QuestionContaner, он должен быть указан в аннотации @XmlSeeAlso , а сам обозначен как XmlType
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;

/**
 *
 * @author str
 * JAXB annotations.
 */
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso(value = {QuestionContaner.class})
public class TestStub {

    protected int id;
    
    // другие поля int и String

    @XmlElement(type = QuestionContaner.class, nillable = true)
    protected QuestionContaner questionContaner;

    public TestStub() {
        //  a no-arg default constructor      
    }

    public TestStub(....) {
        // ...
    }
    
    // gets , sets
  
}

@XmlType
@XmlAccessorType(XmlAccessType.FIELD)
public class QuestionContaner {

    @XmlElement
    protected List<QuestionStub> questionsStub;

    // конструкторы, get, set
}

4. Обработка принятых классов на клиенте.

Клиент принимает объекты в виде SoapObject, вытаскиваем свойства и формируем нужные классы для работы в андроид приложении.
Простые поля можно вытащить по названию , а там же по индексу, по имени конечно удобнее:
Integer id = Integer.parseInt(so.getPropertyAsString("id"));
String name = so.getPropertyAsString("name");
Списки объектов по индексам (если вызывать getProperty с именем пропертиз, то вернется только первый объект. Поэтому:
SoapObject soQuestionContaner = (SoapObject) so.getProperty("questionContaner");
int countQuestions = soQuestionContaner.getPropertyCount();
for (int i = 0; i < countQuestions; i++) {
SoapObject soQuestionStub = (SoapObject) soQuestionContaner.getProperty(i);
 // ...
}
Это в случае т.н объекта-контейнера, которые не содержит ничего кроме списка, поэтому пробежав по всем пропертиз, получим лишь нужный список список. Если класс содержит другие поля, помимо списка, необходимо проходить по диапазону свойств:
final int CONS_PROP = 4; // число фиксированных пропертизов. после них идет список объектов ансвер
  int allProps = so.getPropertyCount(); // всего пропертизов             
  for(int i = CONS_PROP; i < allProps; i++){
    SoapObject soAnswer = (SoapObject)so.getProperty(i);
  //...
  }

5. Передача веб сервису сложных объектов

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

5.1. Клиентские классы

Все передаваемые классы должны реализовывать интерфейс KvmSerializable из библиотеки ksoap2-android. Это относится и к спискам. Далее пример класса со списком также собственных классов для передачи серверу. Со списком примитивов аналогично.
package org.setupit.MLAndroid.wsclient.stubs;

import java.util.Hashtable;
import org.ksoap2.serialization.KvmSerializable;
import org.ksoap2.serialization.PropertyInfo;

/**
 *
 * @author str
 */
public class RezultsComplexStub implements KvmSerializable {

    public static final String NAMEELEMENT = "rezultComplexStub";
    private int learnerTestId;
    protected RezultVector rezults;
    
    private static final int INT_PROPERTY_COUNT = 2;
    private static final int INT_PROPERTY_LEARNER_TEST_ID = 0;
    private static final int INT_PROPERTY_REZULTS = 1;
    
    // названия пропертизов. они должны совпадать с сервачными.
    private final String NAME_PROPERTY_LEARNERTEST_ID = "learnerTestId";
    private final String NAME_PROPERTY_REZULTS = "rezults";

    public RezultsComplexStub() {
    }

    public RezultsComplexStub( int learnerTestId, RezultVector rezults) {
        this.learnerTestId = learnerTestId;
        this.rezults = rezults;
    }    
    
    @Override
    public Object getProperty(int intPropertyIndex) {

        switch (intPropertyIndex) {
            case INT_PROPERTY_LEARNER_TEST_ID:
                return this.getLearnerTestId();
            case INT_PROPERTY_REZULTS:
                return this.getRezults();
        }
        return null;
    }

    @Override
    public int getPropertyCount() {
        return INT_PROPERTY_COUNT;
    }

    @Override
    public void setProperty(int intPropertyIndex, Object objectPropertyNewValue) {

        switch (intPropertyIndex) {
            case INT_PROPERTY_LEARNER_TEST_ID:
                this.setLearnerTestId((Integer) objectPropertyNewValue);
                break;
            case INT_PROPERTY_REZULTS:
                this.setRezults((RezultVector) objectPropertyNewValue);
                break;
            default:
                break;
        }
    }

    @Override
    public void getPropertyInfo(int intPropertyIndex, Hashtable arg1, PropertyInfo info) {
        switch (intPropertyIndex) {
            case INT_PROPERTY_LEARNER_TEST_ID:
                info.type = PropertyInfo.INTEGER_CLASS;
                info.name = NAME_PROPERTY_LEARNERTEST_ID;
                break;
            case INT_PROPERTY_REZULTS:
                info.type = RezultVector.class;
                info.name = NAME_PROPERTY_REZULTS;
                break;
            default:
                break;
        }
    }

   // gets , sets
}
Расширяем вектор , чтобы реализовать интерфейс KvmSerializable :
public class RezultVector extends Vector<VectorItem> implements KvmSerializable {

    public RezultVector() {
    }

    @Override
    public Object getProperty(int index) {
        return this.get(index);
    }

    @Override
    public int getPropertyCount() {
        return this.size();
    }

    @Override
    public void getPropertyInfo(int index, Hashtable properties, PropertyInfo info) {
        info.name = VectorItem.NAMEELEMENT;
        info.type = VectorItem.class;
    }

    @Override
    public void setProperty(int index, Object value) {
        this.add((VectorItem) value);
    }
}
Элементы вектора, это просто классы, аналогично вышеописанному RezultsComplexStu:
public class VectorItem implements KvmSerializable {

    public static final String NAMEELEMENT = "vectorItem";
      
    private Integer answerId;
    private String freeText;
    private int questionId;
     
    private static final int INT_PROPERTY_COUNT = 3;
    private static final int INT_ANSWER_ID      = 0;
    private static final int INT_FREE_TEXT      = 1;
    private static final int INT_QUESTION_ID    = 2;

    // названия пропертизов. они должны совпадать с сервачными.
    private final String NAME_PROPERTY_ANSWER_ID   = "answerId";
    private final String NAME_PROPERTY_FREE_TEXT   = "freeText";
    private final String NAME_PROPERTY_QUESTION_ID = "questionId";
    
    public VectorItem() {
    }

    public VectorItem(Integer answerId, String freeText, int questionId) {
        this.answerId = answerId;
        this.freeText = freeText;
        this.questionId = questionId;
    }
       

    @Override
    public Object getProperty(int intPropertyIndex) {

        switch (intPropertyIndex) {
            case INT_ANSWER_ID:
                return this.getAnswerId();
            case INT_FREE_TEXT:
                return this.getFreeText();
            case INT_QUESTION_ID:
                return this.getQuestionId();
        }
        return null;
    }

    @Override
    public int getPropertyCount() {
        return INT_PROPERTY_COUNT;
    }

    @Override
    public void setProperty(int intPropertyIndex, Object objectPropertyNewValue) {

        switch (intPropertyIndex) {
            case INT_ANSWER_ID:
                this.setAnswerId((Integer)objectPropertyNewValue);
                break;
            case INT_FREE_TEXT:
                this.setFreeText((String)objectPropertyNewValue);
                break;
            case INT_QUESTION_ID:
                this.setQuestionId((Integer)objectPropertyNewValue);
                break;
            default:                
                break;
        }
    }

    @Override
    public void getPropertyInfo(int intPropertyIndex, Hashtable arg1, PropertyInfo info) {
        switch (intPropertyIndex) {
            case INT_ANSWER_ID:
                info.type = PropertyInfo.INTEGER_CLASS;
                info.name = NAME_PROPERTY_ANSWER_ID;
                break;
            case INT_FREE_TEXT:
                info.type = PropertyInfo.STRING_CLASS;
                info.name = NAME_PROPERTY_FREE_TEXT;
                break;
            case INT_QUESTION_ID:
                info.type = PropertyInfo.INTEGER_CLASS;
                info.name = NAME_PROPERTY_QUESTION_ID;
                break;
            default:
                break;
        }
    }

  // gets , sets
}

И не забыть при передаче веб сервису указать меппинг :
PropertyInfo propertyInfo = new PropertyInfo();
 propertyInfo.name = RezultsComplexStub.NAMEELEMENT;
 propertyInfo.type = RezultsComplexStub.class;
 request.addProperty(propertyInfo, rezultComplex);
 // ...
 envenlope.addMapping(NAMESPACE, RezultsComplexStub.NAMEELEMENT, RezultsComplexStub.class);

5.2. Серверные классы

Описанные классы клиента на стороне сервера имеют как бы дубли, все кроме вектора RezultVector, ведь он нужен только для передачи, чтобы сериализовать объекты списка. Достаточно описать аннотациями и сервер поймет что же он принимает.
Класс, содержащий список :
@XmlAccessorType(XmlAccessType.FIELD)  
@XmlType( propOrder={"learnerTestId", "rezults"})  
@XmlRootElement(name = RezultComplexStub.NAMEELEMENT )  
@XmlSeeAlso(value={VectorItem.class})
public class RezultComplexStub {  
    
    public static final String NAMEELEMENT = "rezultComplexStub";
  
    @XmlElement(required=true)
    protected int learnerTestId;

    @XmlElementWrapper(name="rezults", required=false)  
    @XmlElement(required=false, name=VectorItem.NAMEELEMENT)  
    protected List<VectorItem> rezults;  

    public RezultComplexStub() {
    }

   // gets sets
  
} 
Элемент списка :
@XmlAccessorType(XmlAccessType.FIELD)  
@XmlType(propOrder={"answerId", "freeText", "questionId"})  
public class VectorItem {  
  
    public static final String NAMEELEMENT = "vectorItem";
     
    @XmlElement
    protected Integer answerId;
  
    @XmlElement
    protected String freeText;

    @XmlElement(required=true)
    protected int questionId;
    
    public VectorItem() {
    }

    // gets sets
}  

6. Клиент java me : незначительные изменения в коде.

Для мидлета никаких существенных изменений не понадобилось. Все аналогично работает с библиотекой ksoap2 для java me. Несколько отличий все же есть.
Используется HttpTransport вместо HttpTransportSE.
Для получения ответа нужно вызывать SoapObject bodyIn = (SoapObject)evenlope.bodyIn; , иначе будет получен лишь первый объект списка.
Класс расширяющий вектор теперь реализует не параметризированный вектор:
public class RezultVector extends Vector implements KvmSerializable {...}
В остальном все осталось таким же.


Источники :
http://roderickbarnes.com/blog/droid-chronicles-web-services-handling-complex-parameters
http://blog.inflinx.com/2010/10/09/jax-ws-using-weblogic-10-3/
http://code.google.com/p/ksoap2-android/wiki/CodingTipsAndTricks