wtorek, 7 sierpnia 2012

SOAP Webservice Client w CQ 5.4





Ostatnio zajmowałem się uruchomieniem części klienckiej stosu SOAP WebServices na platformie CQ 5.4. Sama platforma oparta jest na Apache Felix, Apache Sling, oraz dodatkowych bundle stworzonych przez Day/Adobe. Pierwotnie próbowano uruchomić na CQ 5.4 stos Apache CXF, który to jest dostępny w wersji OSGI. Jednak duża ilość wymaganych zależności, oraz zależności do tych zależności itd spowodowała, że graf zależności rozrastał się niczym rak. Dodatkowo szukanie w internecie różnego typu bundli eksportujący dany pakiet nie jest najprzyjemniejszym zadaniem. Z racji tego, że nasze serwery instancje CQ są uruchomiane na SUN JRE spróbowałem uruchomić wbudowany stos JAX-WS - czyli tak naprawdę jakąś starszą wersję projektu Metro.

Na potrzeby SOAP WebService korzystałem głównie z Spring WebServices i po raz pierwszy uruchamiałem JAX-WS. Na potrzeby projektu potrzebowałem sparsować WSDL (przesłany nam emailem), wygenerować obiekty JAXB oraz SOAP client stubs, wysłać komunikat, odebrać odpowiedz i wypisać ją na konsole.
  1. Generowanie artefaktów JAX-WS
     
       src/main/generated
    
    
       
     
        org.codehaus.mojo
        build-helper-maven-plugin
        1.7
      
         
       add-source
       generate-sources
       
       add-source
       
       
              ${generatedSourcesFolder} 
     
    
     
     
      
      
      maven-clean-plugin
      
      
       
           ${generatedSourcesFolder}
           false
           
            **
           
          
         
        
       
       
        org.jvnet.jax-ws-commons
        jaxws-maven-plugin
        2.2
        
         
          generate-sources
          
           wsimport
          
         
        
        
         ${basedir}/src/main/resources
         true
         true
         ${generatedSourcesFolder}
         ${basedir}/src/main/resources
         2.0
        
       
          
    
    
    w/w konfiguracja w pom.xml pozwala nam na wygenerowanie obiektów JAXB oraz client stubs w katalogu src/main/generated , który to będzie potraktowany przez Mavena jako dodatkowy katalog ze źródłami aplikacji (zawartość katalogu będzie czyszczona przy w fazie clean ).
    Do katalogu src/main/resource wrzuciłem dwa pliki:
    • myWsdl.wsdl - plik wsdl od klienta
    • binding.xml - który zawiera instrukcję na potrzeby JAXB w kontekście generowanie klasy java na podstawie XML Schema.
      
       
       
              
                  
      
      
      W przypadku gdy JAXB ma obrabiać elementy XML Schema znajdujące się wewnątrz pliku WSDL musimy wyspecyfikować lokalizację tych elementów za pomocą odpowiedniej składni: <położenie-pliku wsdl względem pliku xjb>#types?schema1. W naszym przypadku pliki wsdl oraz xjb leżą obok siebie czyli specyfikujemy myWsdl.wsdl#types?schema1
    Po odpaleniu mvn clean compile - powinniśmy mieć wygenerowane wszystkie artefakty na potrzeby łączenia się z naszym WebServices.
  2. Wywołanie WebServices
    Po wygenerowaniu wszystkie potrzebnych artefaktów możemy próbować wywołać nasze WebServices. Powinna zostać stworzona klasa z anotacją @WebServiceClient o nazwie takiej jak wartość atrybutu name elementu service z pliku wsdl. Klasa ta posłuży nam jako entry point do wołania usług. Standardowo kod kliencki JAX-WS wygląda mniej więcej tak:
    • utwórz za pomocą new instancje klasy która jest oznaczona anotacją @WebServiceClient
    • wywołaj na tej instancji metodę get*, która to zwróci proxy implementujące interfejs oznaczony annotacją @WebService - nazwa interfejsu odpowiada wartości atrybutu name elementu port który jest zawarty w elemencie service
    • przygotowania komunikatu wejściowego za pomocą wygenerowanej klasy ObjectFactory (z anotacją @XmlRegistry), która służy jako fabryka obiektów JAXB
    • wywołanie odpowiedniej metody na proxy przekazując odpowiedni obiekt-komunikat wejściowy
    W moim przypadku niestety musiałe wprowadzić kilka zmian:
    1. zmiana adresu URL endpointa
      BindingProvider bindingProvider = (BindingProvider) <nasze proxy>;
      bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpointAddress);
      
    2. W przypadku tworzenia instancji klasy z anotacją @WebServiceClient za pomocą bezparametrowego konstruktora, plik wsdl jest ładowany z lokalnego dysku - patrz na blok static w klasie z anotacją @WebServiceClient. Niestety jest z tym duży problem gdy uruchomimy nasz kod na innej maszynie od tej, na której to został on zbudowany. Można to rozwiązać na 2 sposoby (przynajmniej mi znane):
      1. za pomocą JAX-WS catalog tak jak zostało to opisane tu . Takie podejście wymaga dodania pliku jax-ws-catalog.xml oraz dodania w naszym pom.xml elementu wsdlLocation. Rozwiązanie wydaję się jak najbardziej odpowiednie, ale jak się później przekonałem niestety nie działa w naszym systemie... Plik katalogu jest zaczytywany przez thread's context classloader, który to niestety go nie widzi. Pewnym obejście jest podmiana thread's context classloadera na czas wywołania WebServices, ale to rozwiązanie wydaje mi się zbyt inwazyjne
      2. można skorzystać z alternatywnego konstruktora dla klasy z anotacją @WebServiceClient i samemu podać URL do pliku wsdl oraz serviceName. Ten drugi można skopiować ze stałych z klasy z anotacją @WebServiceClient. Jeśli chodzi o URL do pliku wsdl, to z racji tego że nasz wsdl znajduje się w src/main/resources to przy kompilacji zostanie przeniesiony do katalogu target i będzie dostępny na classpath - wystarczy wywołać
      3. private URL getWsdlURL() {
          ClassLoader classLoaderToLoadWsdlBy = getClassLoaderToLoadWsdlVia();
          return classLoaderToLoadWsdlBy.getResource("myWsdl.wsdl");
         }
        
        private ClassLoader getClassLoaderToLoadWsdlVia() {
          return <klasa z anotacja @WebServiceClient>.class.getClassLoader();
         }
        
  3. Wypisanie komunikatu wyjściowego na konsole
    JAX-WS w dużej mierze ukrywa przez nami "dokumentową naturę SOAP WebServices". Dostarcza nam za to abstrakcje w postaci proxy, z którym to pracujemy w trybie RPC - wywołujemy metody przekazując parametr/parametry wejściowe i dostajemy wynik. W celach diagnostycznych możemy mieć potrzebę wypisać komunikat wyjściowy za pomocą JAXB Marshaller. Niestety obiekt, który dostaniemy w wyniku wywołania metody na proxy nie koniecznie może być zserializowany do XML przez JAXB - ze względu na brak anotacji @XmlRootElement. Rozwiązanie jest w miarę proste - wystarczy stworzyć odpowiedni obiekt-wrapper:
    
    private void unmarshallToSysout(Object response) throws JAXBException {
      Marshaller m = buildMarshaller(response);
      m.marshal(wrapWithRootElem(response), System.out);
     }
    
     private JAXBElement<Object> wrapWithRootElem(Object response) {
      return new JAXBElement<Object>(new QName("http://www.namespace.com/schema/My",
        "My"), (Class<Object>) response.getClass(), response);
     }
    
     private Marshaller buildMarshaller(Object response) throws JAXBException,
       PropertyException {
      JAXBContext context = JAXBContext.newInstance(response.getClass()
        .getPackage().getName());
    
      Marshaller m = context.createMarshaller();
      m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      m.setProperty(javax.xml.bind.Marshaller.JAXB_ENCODING, "UTF-8");
      return m;
     }
    
    Alternatywnie, można dodać system property com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump=true
  4. Wywołanie WebServices z Cq 5.4
    Wywołania WebServices powinny działać bez problemu w aplikacji standalone (np z JUnit), jednak nie działają na CQ 5.4. Aktualnie mam 3 rozwiązania tego problemu:
    1. Dopisanie do sling.properties dodatkowego wpisu: sling.bootdelegation.com.sun=com.sun.*. Jest to metoda opisana dla CQ 5.5,ale działa także dla CQ 5.4
    2. przygotowanie specjalnego fragment bundle, który to eksportuje wszystkie pakiety z pakietu com.sun - jest ich prawie 400. W przypadku gdy chcemy wołać WebServices z poziomu javy a nie z poziomu jsp musimy ustawić w bundle, który woła WebServices odpowiednie import-package - albo ustawić mu wprost, że wymaga system bundle. To drugie rozwiązanie wydaję się mniej eleganckie, ale w tym przypadku łatwiejsze - wystarczy że w naszym pom.xml dodamy
      
      org.apache.felix
      maven-bundle-plugin
      true
      
       
         system.bundle
       
      
      
      
    3. przygotowałem alternatywny stos JAX-WS, bazują na webservices-rt-1.3.1.jar (nowa wersja jax-ws-ri jest dostępna jako bundle OSGI, ale ma tyle zależności, że ciężko je wszystkie pościągać i poinstalować). Wykorzystanie tak przygotowanego stosu jest niestety dość niewygodne:
      • bundle, który wywołuje WebServices musi mieć ustawiony Require- Bundle na bundle stosu,
      • przy wywołaniu WebServices ustawienie thread's context classloadera na ten, który ładuje/ma dostęp do klas stosu, czyli na przykład ten co załadował klasę: com.sun.xml.ws.spi.ProviderImpl

poniedziałek, 7 maja 2012

DependencyInjectionJunitRulesTestExecutionListener

Ostatnio strasznie spodobały mi się rozszerzenia do @Rule do junit. Pomimo tego że widzę pewne problemy ze stabilnością całego mechanizmu (jak np, zamiana kolejności uruchamiania @Rule a metod @Before i @After) i kilku braków ( m.in. nie działają @Rule zdefiniowane na poziomie suite - dla klas z @RunWith (? extend Suite) ) )  to rozwiązanie jest na prawdę super.
Dzięki wykorzystaniu @Rule możemy bardzo ładnie enkapsulować logikę związaną z wykonaniem testów ,np. współbieżne wykonanie metody testowej , modyfikowanie/zawieszanie metod testowych. Mechanizm jest na tyle uniwersalny, że w przyszłości annotacje @Before/@After maja zostać uznane za deprecated na rzecz wykorzystania @Rule. Aktualnie junit w wersji 4.10 wymaga aby anotacja @Rule była umieszczona na publicznym nie statycznym property typu TestRule lub MethodRule (deprecated). Co ciekawe wygląda na to, że @Rule działają bardzo dobrze z Spring TestContext Framework (przynajmniej dla junit-4.10 oraz SpringJUnit4ClassRunner z wersji springa 3.1.1).  Integracja może wyglądać w dwojaki sposób:
  • DI dla implementacji interfejsu TestRule
  • TestRule zdefiniowane bezpośrednio w spring
W pierwszym przypadku stworzyłem bardzo prosty listener który  injectuje beany do @Rule  i @ClassRule:
public class DependencyInjectionJunitRulesTestExecutionListener extends
        AbstractTestExecutionListener {
   @Override
    public void prepareTestInstance(final TestContext testContext)
            throws Exception {
        Object testInstance = testContext.getTestInstance();
        List<TestRule> ruleFields = getRuleFields(testInstance);
        doInjection(testContext, ruleFields);
    }

    private void doInjection(final TestContext testContext,
            List<TestRule> ruleFields) {
        AutowireCapableBeanFactory beanFactory = testContext
                .getApplicationContext().getAutowireCapableBeanFactory();
        for (TestRule testRule : ruleFields) {
            beanFactory.autowireBeanProperties(testRule,
                    AutowireCapableBeanFactory.AUTOWIRE_NO, false);
            beanFactory.initializeBean(testRule, testContext.getTestClass()
                    .getName());
        }
    }

    private List<TestRule> getRuleFields(Object testInstance) {
        TestClass testClass = new TestClass(testInstance.getClass());
        return testClass.getAnnotatedFieldValues(testInstance, Rule.class,
                TestRule.class);
    }

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        List<TestRule> classRuleFields = getClassRuleFields(testContext);
        doInjection(testContext, classRuleFields);
    }

    private List<TestRule> getClassRuleFields(TestContext testContext) {
        TestClass testClass = new TestClass(testContext.getTestClass());
        return testClass.getAnnotatedFieldValues(null, ClassRule.class,
                TestRule.class);
    }
   


}
Implementacja bazuję na dostępnym out-of-box DependencyInjectionTestExecutionListener oraz TestClass. Wszystko wygląda nieźle, ale niestety nie do końca działa :(. Brakuję na pewno reinjectowania beanów dla @ClassRule  w przypadku gdy nasza metoda/klasa używa @DirtiesContext. Implementacja nie powinna być trudna gdyż DirtiesContextTestExecutionListener ustawia odpowiednią flagę na TestContext (niestety ta flaga jest usuwana przez DependencyInjectionTestExecutionListener, czyli trzeba albo nasz listener ustawić przed DependencyInjectionTestExecutionListener albo rozszerzyć ten ostatni).
Większym problem jest jednak to, że aktualnie dla Spring 3.1.1.RELEASE odpowiednie metody klasy TestExecutionListener, a dokładnie metoda beforeTestClass jest wywoływana po @ClassRule. "Winna" temu jest metoda classBlock w klasie ParentRunner, po której to dziedziczy SpringJUnit4ClassRunner. Zgodnie z tym co można znaleźć w javadoc @ClassRule opakowują uruchomienie @BeforeClass/@AfterClass, a te z kolei SpringJUnit4ClassRunner opakowuje w wywołanie TestExecutionListener#beforeTestClass. Reasumując: najpierw zostaną uruchomiene @ClassRule, następnie TestExecutionListener#beforeTestClass a na końcu metody @BeforeClass. Metody definiujące zadaną kolejność oraz wywołania są protected, czyli wygląda na to, że zmiana jest jak najbardziej do zrobienia. W innym przypadku, tzn gdy nie potrzebujemy injectowania dla @ClassRule z przedstawionego powyżej kodu można spokojnie wyrzucić metodę beforeTestClass.
Jeśli chodzi o drugie podejście to sprawa wygląda całkiem podobnie. Tzn. w fazie TestExecutionListener#prepareTestInstance jest wykonywane DI, które to może injectować do property z anotacją @Rule. Jeśli chodzi o @ClassRule to DI dla pól statycznych nie działa by design. Są pewne obejścia, ale chyba bez jakiegoś większego plumbingu się nie obejdzie.
EDIT: aktualnie pojawiła się potrzeba stworzenia odpowiedniego test fixture, który to byłby dostępny dla kilku metod testowych. Można to łatwo zaimplementować jako TestRule albo metodę @Before, jednak ze względu na to, że tworzenie tego fixure może być czasochłonne lepiej zrobić to tylko raz. W szczególności chodzi o stworzenie i "ustawienie" stanu odpowiedniego bytu w systemie przy wykorzystaniu Selenium (Webdriver) aby testy w danej klasie mogły bazować na tak przygotowanym bycie. W przypadku JUnit najsensowniejsze wydaje się wykorzystanie @BeforeClass. W moim przypadku instancja WebDrivera jest injectowana przez Spring TestContext Framework, a skoro injectowanie do pól statycznych (tak aby były do użycia przez @BeforeClass) nie działa out-of box zdecydowałem się na to aby tworzyć test fixture za pomocą ClassRule - DI dla ClassRule zostało zaimplementowane w DependencyInjectionJunitRulesTestExecutionListener#beforeTestClass. Problemem jest tylko to, że w aktualnej implementacji Spring TestContext Framework bazuje na Junit 4.5, czyli SpringJUnit4ClassRunner uruchamia TestExecutionListener#beforeTestClass przed @BeforeClass, ale po odpaleniu ClassRule. Zmiana tej kolejności okazała się stosunkowo prosta, ale potrzebowałem stworzyć nowy Runner:
public class RunSpringAroundClassRulesJUnit4ClassRunner extends
        SpringJUnit4ClassRunner {

    public static class AroundTestClassCallbacks extends Statement {
        private final Statement statement;

        public AroundTestClassCallbacks(Statement next,
                TestContextManager testContextManager) {
            this.statement = new RunAfterTestClassCallbacks(
                    new RunBeforeTestClassCallbacks(next, testContextManager),
                    testContextManager);
        }
        @Override
        public void evaluate() throws Throwable {
            statement.evaluate();
        }
    }

    public RunSpringAroundClassRulesJUnit4ClassRunner(Class<?> clazz)
            throws InitializationError {
        super(clazz);
    }

    @Override
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement = childrenInvoker(notifier);
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        statement = withAroundTestClass(statement);
        return statement;
    }

    private Statement withAroundTestClass(Statement statement) {
        return new AroundTestClassCallbacks(statement, getTestContextManager());
    }

    @Override
    protected Statement withBeforeClasses(Statement statement) {
        List<FrameworkMethod< befores = getTestClass().getAnnotatedMethods(
                BeforeClass.class);
        return befores.isEmpty() ? statement : new RunBefores(statement,
                befores, null);
    }

    @Override
    protected Statement withAfterClasses(Statement statement) {
        List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(
                AfterClass.class);
        return afters.isEmpty() ? statement : new RunAfters(statement, afters,
                null);
    }

    private Statement withClassRules(Statement statement) {
        List<TestRule> classRules = classRules();
        return classRules.isEmpty() ? statement : new RunRules(statement,
                classRules, getDescription());
    }
}