XML w J2EE

Karol Gilarski

Jedną z części J2EE jest Java XML Pack. Składają się na niego następujące elementy:

Java API for XML Processing (JAXP)

Umożliwia pracę z danymi zapisanymi w XML'u za pomocą:

SAX (Simple API for XML Parsing)

Umożliwia proste parsowanie dokumentów XML za pomocą parsera opartego na obsłudze zdarzeń. Parserowi podczepiamy napisaną przez nas klasę o odpowiednim interfejsie. Podczas parsowania parser będzie wywoływał opowiednie metody tej klasy.

Podstawowe zdarzenia, które można obsłużyć:

Podczepiając handler pod w/w zdarzenia mamy dostęp do wszystkich elementów dokumentu XML'a. Zaletą SAX'a jest prostota, natomiast wadą dostęp do danych w sposób sekwencyjny.

Przykład: (Uproszczone Echo - wychwytuje wszystkie tagi otwierające)

import org.xml.sax.*;	
import org.xml.sax.helpers.DefaultHandler;	

public class MyHandler extends DefaultHandler
{	
	public void startElement (
		String namespaceURI,	
		String sName, 	// simple name	
		String qName, 	// qualified name	
		Attributes attrs)	
	throws SAXException	
	{	
	   String eName = sName; // element name	
	   if ("".equals(eName)) eName = qName; // not namespaceAware	
	   emit("<"+eName);	
	   if (attrs != null) {	
	      for (int i = 0; i < attrs.getLength(); i++) {	
	         String aName = attrs.getLocalName(i); // Attr name	
	         if ("".equals(aName)) aName = attrs.getQName(i);	
	         emit("");	
	         emit(aName+"=\"+attrs.getvalue(i)+"\"");	
	      }	
	   }	
	   emit(">");	
	}

	private void emit(String s) {
		// ... print string 
	}	

}

Obsługa błędów

W przypadku źle zformatowanego dokumentu XML, parser zgłasza wyjątek SAXParseException. Razem z nim przesyłane są informacje o błędzie:

Przykład:
try {
   ...	
} catch (SAXParseException spe) {	
   // Blad wygenerowany przez parser	
   System.out.println("* Parsing error" 	
      + ", line " + spe.getLineNumber()	
      + ", uri " + spe.getSystemId());	
   System.out.println("   " + spe.getMessage() );	
}

Podlaczenie handlera

Przykład podlaczenie handlera i uruchomienie parsera na danym dokumencie XML. Jako źródło danych może służyć plik, strumień, tj. Stream bądź też StreamReader.

import java.io.*;
import javax.xml.parsers.*;
	
	...
	String filename = "test.xml";
	
	// nasz handler 
	DefaultHandler handler = new MyHandler();
		
	// pobranie fabryki parserow  
	SAXParserFactory factory = 
		SAXParserFactory.newInstance();	
	
	// parsowanie	 
	SAXParser saxParser = factory.newSAXParser();	
	saxParser.parse ( new File(filename), handler );

DOM (Document Object Model)

Jak już wspomniałem podstawową wadą SAX'a jest sekwencyjny dostęp. Jednak istnieje inna metoda czytania XML'a oparta na DOM'ie.

DOM jest to zbiór interfejsów, które odwzorowują strukturę dokumentu XML. Podstawowe interfejsy to Document, Element i Node. Obiekty tych interfejsów tworzą drzewo, które również nazywa się DOM'em.

Tworzenie DOM'a

Istnieją w praktyce dwie metody tworzenia struktury DOM. Pierwsza z nich polega na zczytaniu gotowych danych z innego źródła, np. z pliku. Druga metoda polega na stworzeniu struktury z poziomu programu, a więc za pomocą odpowiedniego API.

Zczytywanie DOM'a

Tak jak w przypadku SAX'a źródłem danych ogólnie jest strumień.

import org.w3c.dom.*;
import javax.xml.parsers.*;

	...
	String filename = "test.xml";
	Document document;
	DocumentBuilderFactory factory = 
		DocumentBuilderFactory.newInstance();
	try {
		// tworzymy builder'a i zczytujemy dane 
		DocumentBuilder builder = 
			factory.newDocumentBuilder();	
	   	document = builder.parse ( new File(filename) );	
	} 
	catch (SAXParseException e) {
		...	
	}

Tworzenie DOM'a z poziomu kodu

Jak się można domysleć odpowiada to tworzeniu drzewa. A więc zaczynamy od korzenia, a następnie doczepiamy kolejne kawałki, przy czym możemy to robić w każdej sensownej kolejności.

import org.w3c.dom.*;
	
	Document document;
	DocumentBuilderFactory factory = 
		DocumentBuilderFactory.newInstance();	
	try {	
		// polozenie fundamentow 
        	DocumentBuilder builder = 
			factory.newDocumentBuilder();	
		document = builder.newDocument(); } 
		
		// budowa struktury 
		Element root = 	
			(Element) document.createElement(
				"rootElement"); 	
		document.appendChild(root);	
	        root.appendChild( 
			document.createTextNode("Some") );	
	        root.appendChild( 
			document.createTextNode(" ")    );	
	        root.appendChild( 
			document.createTextNode("text") );	
	
		// taki dokument bedzie rownowazny tresciowo  
		root.appendChild (
			document.createTextNode ("Some text"));
		
		// .. natomiast calkowicie rownowazny  
		// dopiero po normalizacji 
		document.getDocumentElement().normalize();	
	} 
	catch (ParserConfigurationException pce) {	
		...	
	}

Jak można zauważyć dane w węzłach odpowiadających tagom XML'a, przechowywane są jako odrębne węzły. Z tego powodu dokument XML'a może być równoważny kilku różnym DOM'om.

Czytanie DOM'a i modyfikacja

DOM w przeciwieństwie do SAX'a może być obchodzony nie tylko sekwencyjnie. Umożliwia to typowy dla drzew interfejs Node:

Poza tym mamy możliwość dochodzenia do konkretnego węzła za pomocą metody getElementsByTagName interfejsu Document. W efekcie dostajemy listę węzłów w kolejności odpowiadającej przechodzeniu dokumentu XML.

Niestety metoda ta ma ograniczenia, polegające na tym, że zawsze przeszukiwany jest cały dokument oraz jeśli szukamy konkretnego elementu, to musimy sobie ręcznie odfiltrować trafienia.

Ponadto DOM może być modyfikowany w dowolny sposób za pomocą metod:

Wszystkie te metody dotyczą jednak bardzo niskopoziomowych zmian, gdyż ograniczają się do manipulacji poszczególnych węzłów drzewa.

XSLT (XML Stylesheet Language Transformations)

Java API for XSLT umożliwia transformowanie dokumentów XML za pomocą arkuszy XSL w sposób zgodny ze specyfikacją XSLT, zdefiniowaną przez W3C.

Przykład:

import javax.xml.transform.*;	

	// zbudowanie transformera z arkuszu XSL 
	TransformerFactory transFactory = 
		TransformerFactory.newInstance();	
	Transformer transformer = 
		transFactory.newTransformer(
			new StreamSource("myXSL.xsl"));

	// Document doc = ... 
	DOMSource source = new DOMSource(doc);
	
	// przygotowanie miejsca na wynik 
	File newXML = new File("newXML.xml");	
	FileOutputStream os = new FileOutputStream(newXML);
	StreamResult result = new StreamResult(os);

	// uruchomienie transformacji 
	transformer.transform(source, result);

Dane wejściowe, tj. dane XML, mogą być pobierane zarówno ze strumienia (np. z pliku) jak i z wcześniej zbudowanej struktury DOM.

Przykład zastosowania XSLT:

Serwlet generuje wewnątrz siebie dokument XML, a następnie stosuje zadaną transformację z wykorzystaniem arkuszy XSL w celu uzyskania odpowiedniej prezentacji danych:

  • Prezentacja A
  • Prezentacja B

    Kod przykładu (z pominięciem klas dostarczających danych)

    Podsumowanie

    Java API for XML-based RPC (JAX-RPC)

    Umożliwia zdalne wywoływanie metod za pomocą SOAP (Simple Object Access Protocol). Servant udostępniający metody przedstawia swój interfejs oraz lokalizację w postaci dokumentu WSDL (Web Services Description Language). Klient wysyła żądanie zgodne z SOAP'em, dostaje wynik również poprzez SOAP'a.

    Metoda ta jest bardzo podobna do RMI, z tym że klient może być napisany w dowolnym języku.

    Dla servantu tworzy się interfejs identyczny co w przypadku RMI:

    import java.rmi.Remote; 	
    import java.rmi.RemoteException;
    
    public interface CoffeeOrderIF extends Remote { 	
       public Coffee [] getPriceList() 	
       	throws RemoteException; 	
       public String orderCoffee(String coffeeName, int quantity)	
           	throws RemoteException; 	
    }
    

    Następnie kodujemy servant implementujący wcześniej zdefiniowany interfejs:

    public class CoffeeOrderImpl 
    	implements CoffeeOrderIF { 	
       public Coffee [] getPriceList() 
       	throws RemoteException; {	
          . . .	
       }
    
       public String orderCoffee(String coffeeName, int quantity)	
       	throws RemoteException; {	
          . . .	
       }	
    }   
    

    Podobnie jak w RMI mamy klasy tie po stronie serwera oraz klasy stub po stronie klienta (o ile klient jest napisany w Javie). Klasy te uzyskuje się poprzez wywołanie na uprzednio przygotowanym dokumencie WSDL narzędzia mapującego (xrpcc).

    Dokument WSDL jest to dokument XML'a, który uzyskuje się poprzez wywołanie wspomnianego narzędzia na pliku ze zdefiniowanym interfejsem serwleta (w naszym przykładzie: CoffeeOrderIF). Zanim dokument WSDL będzie gotowy, należy go uzupełnić o takie informacje jak lokalizacja serwletu.

    Servanty mogą być wystawiane na świat albo w postaci serwletów, albo jako EJB. W przypadku serwletów dokument WSDL będzie zawierał adres serwleta. W takim przypadku w momencie wywołania nie trzeba podawać lokalizacji serwletu.

       CoffeeOrderIF coffeeOrder = new	
          CoffeeOrderServiceImpl().getCoffeeOrderIF();
    

    Można również wywołać xrpcc bezpośrednio na interfejsie, w tym przypadku klient musi sam podać lokalizację serlwletu:

       HelloIF_Stub stub = (HelloIF_Stub)	
          (new HelloWorld_Impl().getHelloIF());	
       stub._setProperty(	
          javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY,
          "http://localhost:8080/jaxrpc-dynamic/jaxrpc/HelloIF");	
       System.out.println(stub.sayHello("Duke!"));
    

    Klasy HelloIF_Stub oraz HelloWorld_Impl są to klasy wygenerowane przez program xrpcc.

    Sam proces instalacji serwletu jest bardziej skomplikowany, niż ma to normalnie miejsce. Program xrpcc tworzy również plik konfiguracyjny, do którego odwołanie trzeba zamieścić w pliku web.xml opisującym serwlet dla serwera aplikacji.

    ...
    <servlet> 
    ...
    	<init-param>
    		<param-name>configuration.file</param-name> 
    		<param-value>
    			/WEB-INF/HelloWorld_Config.properties 
    		</param-value> 
    	</init-param>
    ...
    </servlet>
    ...
    

    A gdzie jest XML?

    Poza tym, że WSDL jest dokumentem XML, cały SOAP oparty jest na XML'u. Polega on na przesyłaniu dokumentów XML'owych w odpowiednim formacie. W przypadku serwletów wszystko chodzi po HTTP.

    Java API for XML Messaging (JAXM)

    Umożliwia przesyłanie dokumentów XML'a poprzez Internet. Oparty jest na SOAP'ie.

    Wiadomość w formacie SOAP ma ogólnie następującą postać:

    I. SOAP message
         A. SOAP part
             1. SOAP envelope
                  a. SOAP header 
                  b. SOAP body
         B. Attachment part 1
         C. Attachment part 2
         ...
    

    Zarówno nagłówek jak i załączniki są opcjonalne.

    Przesyłanie wiadomości może odbywać się na dwa sposoby:

    Przesyłanie bezpośrednie

    Jest to prostsze rozwiązanie. Klient tworzy połączenie bezpośrednio do odbiorcy. Przesyłanie wiadomości jest sychroniczne, tj. klient oczekuje od razu odpowiedzi.

    Przykład:

       // tworzenie polaczenia 
       SOAPConnectionFactory factory =   
          SOAPConnectionFactory.newInstance();
       SOAPConnection con = 
          factory.createConnection();
       
       // tworzenie wiadomosci 
       MessageFactory messageFactory = 
          MessageFactory.newInstance();   
       SOAPMessage m = 
          messageFactory.createMessage();
       
       // dostanie sie do tresci wiadomosci ... 
       SOAPPart sp = m.getSOAPPart();   
       SOAPEnvelope envelope = sp.getSOAPEnvelope();   
       SOAPBody body = envelope.getSOAPBody();   
       
       // stworzenie tresci  
       SOAPBodyElement bodyElement = body.addBodyElement(   
          envelope.createName("text", "hotitems",   
          "http://hotitems.com/products/gizmo");   
       bodyElement.addTextNode("some-xml-text");
       
       // wyslanie wiadomosci pod wskazany adres 
       SOAPMessage response =    
          soapConnection.call(message, urlEndpoint);
    

    Jako treść wiadomości można też przesłać przygotowany wcześniej DOM, bądź też podać StreamSource jako źródło danych XML'owych.

       Document doc = ... // uprzednio przygotowany DOM
       DOMSource domSource = new DOMSource(doc);
       soapPart.setContent(domSource);
    

    Do wiadomości można podłączyć załączniki, np. pliki:

       SOAPMessage m = ... 
       URL url = new URL("http://foo.bar/img.jpg");	
       DataHandler dh = new DataHandler(url);	
       AttachmentPart attachPart = m.createAttachmentPart(dh);	
       m.addAttachmentPart(attachPart);
    

    ... bądź też dowolny strumień danych:

       InputStream is = ...
       AttachmentPart attachPart = m.createAttachmentPart();
       attachPart.setContent (is); 
       m.addAttachmentPart(attachPart);
    

    Dostawca wiadomości

    W przypadku zastosowania dostawcy wiadomości wysyłanie wiadomości jest asynchroniczne, tzn. że jest nieblokujące oraz najpierw wiadomość jest przesyłana do dostawcy. Dopiero ten przesyła ją do adresata.

    Dostawca działa w sposób ciągły, tzn. może przyjmować wiadomości dla klienta, nawet jeśli ten nie jest podłączony do dostawcy. W takiej sytuacji dostawca przechowa wiadomość i prześle ją dopiero w momencie, kiedy klient będzie dostępny.

    Użycie dostawcy jest warunkiem koniecznym do odbierania wiadomości. Wiążę się z tym jednak konieczność uruchamiania klienta w kontenerze, np. serwera aplikacji.

    Przykład użycia dostawcy:

       ProviderConnectionFactory pcFactory = 
          ProviderConnectionFactory.newInstance();	
       ProviderConnection pcCon = 
          pcFactory.createConnection();
       
       // stworzenie wiadomosci 
       SOAPMessage msg = ... 
       
       pcCon.send(msg);
    

    Adresat przesyłanej wiadomości ustalany jest na podstawie sekcji header elementu SOAPEnvelope.

    Java API for XML Registries (JAXR)

    Umożliwia rejestrację oraz odszukiwanie różnych serwisów sieciowych. Stosuje sie m.in. w interakcjach B2B (Business to Business).

    Architektura JAXR składa się z :

    Klient JAXR

    Przykład:

    import javax.xml.registry.*;	
    
       ConnectionFactory connFactory =    
           ConnectionFactory.newInstance();
    
       // ustawienie namiarow na przykładowy rejestr 
       Properties props = new Properties();   
       props.setProperty("javax.xml.registry.queryManagerURL",   
          "http://www-3.ibm.com/services/uddi/v2beta/inquiryapi");
       props.setProperty("javax.xml.registry.lifeCycleManagerURL",
          "https://www-3.ibm.com/services/uddi/v2beta/protect/publishapi");   
       props.setProperty("javax.xml.registry.factoryClass", 
          "com.sun.xml.registry.uddi.ConnectionFactoryImpl");
       
       // stworzenie polaczenia
       connFactory.setProperties(props);
       Connection connection = connFactory.createConnection();
    
       // pobranie odpowiednich interfejsow 
       RegistryService rs = connection.getRegistryService();   
       BusinessQueryManager qm = 
          rs.getBusinessQueryManager();   
       BusinessLifeCycleManager lcm =    
          rs.getBusinessLifeCycleManageger();
    

    QueryManager umożliwia zadawanie pytań rejestrowi.

    LifeCycleManager pozwala na modyfikację rejestru.

    Rejestracja serwisu

    W ramach modyfikacji rejestru, możemy zarejestrować własną organizację:

       // stworzenie organizacji
       Organization org = lcm.createOrganization("The Coffee Break");   
       org.setDescription(   
          "Purveyor of only the finest coffees. Established 1895");
    
       // sklasyfikowanie organizacji
       ClassificationScheme cScheme =    
          bqm.findClassificationSchemeByName("ntis-gov:naics");
    
       Classification classification = 
          (Classification)lcm.createClassification(cScheme, 	
             "Snack and Nonalcoholic Beverage Bars", "722213");
      
       Collection classifications = new ArrayList();
       classifications.add(classification);
       org.addClassifications(classifications);
       
       // rejestracja
       Collection orgs = new ArrayList();
       orgs.add(org);	
       lcm.saveOrganizations(orgs);
    

    Aby rejestr zmodyfikować należy się z reguły uwierzytelnić. Wykonuje się to poprzez metodę setCredentials obiekty Connection:

       String username = ...
       String password = ...	
       
       PasswordAuthentication passwdAuth =	
          new PasswordAuthentication(
             username, 	
             password.toCharArray());	
       	
       Set creds = new HashSet();	
       creds.add(passwdAuth);	
       connection.setCredentials(creds);
    

    Przepytywanie rejestru

    Po podlączenie do rejestru, możemy taki rejestr przepytać.

       // zdefiniowanie kryteriow wyszukiwania
       Collection findQualifiers = new ArrayList();	
       findQualifiers.add(FindQualifier.CASE_SENSITIVE_MATCH),
       
       Collection namePatterns = new ArrayList(),
       namePatterns.add("%Coffee%"); 
       
       // wyszukanie organizacji w oparciu
       // o nazwę oraz zdefiniowaną klasyfikację
       BulkResponse response = bqm.findOrganizations(findQualifiers,
          namePatterns, null, classifications, null, null);
       Collection orgs = response.getCollection();
    

    JAXR obsługuje również zapytania SQL:

       DeclarativeQueryManager dqm = rs.getDeclarativeQueryManager();
       Query query = dqm.createQuery(Query.QUERY_TYPE_SQL,
          "SELECT id FROM RegistryEntry WHERE name LIKE %Coffee% " +
             "AND majorVersion >= 1 AND " +	
                "(majorVersion >= 2 OR minorVersion >= 3)");	
       BulkResponse response2 = dqm.executeQuery(query);
    

    W celu zapewnienia współpracy z klientami innymi niż napisanymi w Javie, komunikacja pomiędzy klientem JAXR a rejestrem oparta jest na JAXM.

    Dalsze informacje

    The Java Web Services Tutorial - podstawowe źródło referatu, strona SUN Microsystems poświęcona J2EE.