Użytkownicy usług sieciowych w Javie często używają narzędzia WSDL2Java – nie ma chyba prostszego sposobu na ‚napisanie’ klienta usługi niż ten. Wystarczy plik WSDL i jedna komenda, i ciach - mamy klienta. Co jednak, jeśli nasza usługa polega na wymianie obiektów naszych klas? Takie obiekty będą zwracane, albo przekazywane jako argument w metodach wykonanych przez usługę. Oznacza to, że – zgodnie z WSDL – klient i serwer muszą mieć definicje tych klas. Niestety, w praktyce oznacza to też, że ta sama klasa będzie zdefiniowana dwa razy (dla klienta i dla serwera osobno). Problem zaczyna się wtedy, gdy chcemy naszą klasę wyposażyć w jakieś dodatkowe metody, które chcielibyśmy wykorzystać zarówno po stronie serwera, jak i klienta.

Przykładowa sytuacja:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package net.schowek.ws.server;             
 
public class Foo 
{ 
    private int bar;         
 
    public int getBar() 
    { 
        return bar; 
    }           
 
    public void setBar(int bar) 
    { 
         this.bar = bar; 
    }              
 
    public void blah() 
    { 
        System.out.println("BLAH: "+bar); 
    } 
}

Niech nasza klasa serwisu wygląda tak:

1
2
3
4
5
6
7
8
9
10
11
package net.schowek.ws.server;        
 
public class FooService 
{ 
    public Foo getFoo(int b) 
    { 
        Foo f = new Foo(); 
        f.setBar(b) 
        return f; 
    } 
}

Jeśli nasza usługa udostępni obiekt klasy Foo, to WSDL2Java wygeneruje kod w pakiecie np. net.schowek.ws.client, w którym zdefiniowana będzie klasa Foo, ale bez metody blah() (wygeneruje tylko gettery i settery).
A szkoda, bo całą funcjonalność prawdopodobnie będziemy musieli klientowi implementować od nowa.
Spróbujmy więc zaplanować inną architekturę. Niech nasza klasa Foo powędruje do pakietu net.schowek.ws.common, a pakiet serwera niech zaimplementuje jedynie FooService, zwracającą obiekt klasy Foo w pakiecie wspólnym. Byłoby super, gdyby WSDL2Java wygenerował nam teraz kod, który również dziedziczyłby po pakiecie net.schowek.ws.common.
Jak tego dokonać? No właśnie.
Twórcy Axis’a, w którego skład wchodzi WSDL2Java pomyśleli o tym i pozwolili użytkownikowi na skonstruowanie własnych generatorów kodu. Wystarczy zaimplementować własną klasę WSDL2Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package net.schowek.ws.tools;        
 
public class MyWSDL2Java extends WSDL2Java 
{ 
    public MyWSDL2Java()  { }            
 
    protected Parser createParser() 
    { 
        return new MyEmitter(); 
    }        
 
    public static void main(String args[]) 
    { 
        MyWSDL2Java w2j = new MyWSDL2Java(); 
        w2j.run(args); 
    } 
}

Własną klasę emitera:

1
2
3
4
5
6
7
8
9
10
11
package net.schowek.ws.tools;        
 
public class MyEmitter extends Emitter 
{ 
    public MyEmitter() 
    { 
        MyGeneratorFactory factory = new MyGeneratorFactory(); 
        setFactory(factory); 
        factory.setEmitter(this); 
    } 
}

Własną fabrykę generatorów:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package net.schowek.ws.tools;        
 
public class MyGeneratorFactory extends JavaGeneratorFactory 
{ 
    public Generator getGenerator(TypeEntry type, SymbolTable symbolTable) 
    { 
        if (include(type.getQName())) 
        { 
            return new MyGeneratorWriter(emitter, type, symbolTable); 
        } 
        else 
        { 
            return new NoopGenerator(); 
        } 
    } 
}

… i wreszcie – sam generator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package net.schowek.ws.tools;   
 
public class MyGeneratorWriter extends JavaTypeWriter 
{ 
    public MyGeneratorWriter( Emitter e, TypeEntry t, SymbolTable s ) 
    { 
        super( e, t, s ); 
    }   
 
    public JavaWriter getBeanWriter( Emitter emitter, TypeEntry type, TypeEntry base ) 
    { 
        Vector elements = type.getContainedElements(); 
        Vector attributes = type.getContainedAttributes(); 
        Boolean isComplexFault = (Boolean)type.getDynamicVar( 
                              JavaGeneratorFactory.COMPLEX_TYPE_FAULT );   
 
        if( ( isComplexFault != null ) && isComplexFault.booleanValue() ) 
        { 
            return super.getBeanWriter( emitter, type, base ); 
        }   
 
        return new MyBeanWriter( emitter, type, elements, base, attributes, 
            getBeanHelperWriter( emitter, type, elements, base, attributes, false ) ); 
    } 
}

i writer, czyli klasę odpowiedzialną za generowanie odpowiedniego kodu (w tym przypadku – klasy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package net.schowek.ws.tools;        
 
public class MyBeanWriter extends JavaBeanWriter 
{ 
    protected MyBeanWriter( Emitter e, TypeEntry t1, Vector v1, 
                            TypeEntry t2, Vector v2, JavaWriter w ) 
    { 
        super( e, t1, v1, t2, v2, w ); 
        this.enableSimpleConstructors = false; 
        this.enableDefaultConstructor = true; 
        this.enableFullConstructor = false; 
        this.enableMemberFields = false; 
        this.enableGetters = false; 
        this.enableSetters = false; 
    }            
 
    protected String getImplementsText() 
    { 
        String s = " extends net.schowek.ws.common." 
                 + this.getClassName()+ " " 
                 + super.getImplementsText(); 
        return s; 
    }        
 
    protected void writeAccessMethods() 
    { 
        pw.println("    public " 
                 + className 
                 + "( net.schowek.ws.common." 
                 + className 
                 + " old ) { " );        
 
        for (int i = 0; i < names.size(); i += 2) 
        { 
            String variable = (String) names.get(i + 1); 
            String typeName = (String) names.get(i); 
            String var = Utils.capitalizeFirstChar(variable) 
            String getter = "";        
 
            if( typeName.equals( "boolean" )) 
            { 
                getter = "old.is" + var + "()"; 
            } 
            else 
            { 
                getter = "old.get" + var + "()"; 
            }        
 
            pw.println("           this.set" + var + "( "+getter+" );");        
 
            if (i >= names.size() - 2) 
            { 
                break; 
            } 
        }        
 
        pw.println("    }"); 
        pw.println(); 
        super.writeAccessMethods(); 
    } 
}

Jak widać w konstruktorze klasy MyBeanWriter, generatory są całkiem fajnie konfigurowalne, dzięki czemu nasz kod może wyglądać naprawdę różnie. Żeby ‚wstrzyknąć’ do wygenerowanego kodu informację o dziedziczeniu z pakietu wspólnego użyłem przesłonięcia metody getImplementsText() (skorzystałem z tego, że informacje o dziedziczeniu jest umieszczanie w Javie zaraz przed deklaracją i implementowaniu interfejsu) – ta metoda wywoła się na pewno, bo WSDL2Java już tak ma ;)
Ze względu na fakt, że nasza wygenerowana klasa będzie dziedziczyć po klasie z pakietu wspólnego, nie ma potrzeby deklarowania jej własności, dlatego wyłączam enableMemberFields (należy jednak pamiętać, że pierwowzór klasy Foo w pakiecie common musi mieć własności o dostępie protected, a nie private). Ponadto, skoro włączyłem już enableDefaultConstructor, to postanowiłem wykonać też metodę, która wygeneruje mi konstruktor kopiujący z obiektu klasy nadrzędnej. W ten sposób zapewniłem możliwość rzutowania moich klas klienta i serwera w obie strony (w końcu mają być takie same, prawda?). Kod można oczywiście poprawić tak, by nazwę pakietu wspólnego dało się podawać np. jako opcję przy uruchomieniu, ale najważniejsze, że robi to co chciałem i tak jest fajnie :)