Marzec
08
2009

Zapis do pliku XML – część IV

Słowa kluczowe: , | Kategorie: Java
No Gravatar

części IIIII utworzyłem zestaw klas, na podstawie których będę chciał utworzyć plik XHTML.

Dzisiaj miałem się omówić sam zapis. Jednak mnogość i złożoność funkcji pomocniczych wymaga, abym najpierw zajął się nimi.

Klasa SimpleText

Na początek prosta klasa służąca do zamknięcia obiektów typu java.lang.String wewnątrz typu opatrzonego adnotacją @Text:

package xml;

import xml.annotation.Text;

@Text
public class SimpleText {
    private String text;

    public SimpleText(String text) {
        this.text = text;
    }

    @Text
    public String getText() {
        return this.text;
    }
}

SimpleText przyda się w momencie podczas tworzenia listy zawartości attributeu XML. Wówczas będę potrzebował, aby każdy obiekt wewnątrz listy był opatrzony adnotacją @attribute albo @Text.

Klasa XMLException

Aby trochę uporządkować zarządzanie wyjątkami, utworzę nowy – związany z zapisem do XML:

package xml;

public class XMLException extends Exception {
    private static final long serialVersionUID = 1L;

    public static final String ANNOTATION_NOT_FOUND =
        "Annotation not found: %s.";
    public static final String MEMBER_ACCESS_DENIED =
        "Could not access the members: %s.%s.";
    public static final String UNEXPECTED_ARGUMENTS =
        "The annotated method should not have parameters: %s.%s.";
    public static final String INVOCATION_FAILED =
        "The annotated method invocation has raised an exception: %s.%s.";

    public XMLException(String message) {
        super(message);
    }
}

Klasa ta zawiera konstruktor ustawiający komunikat wyjątku oraz stałe reprezentujące używane komunikaty.

Kilka słów wyjaśnienia wymaga definicja stałej serialVersionUID. Ponieważ klasa Exception (oraz wszystkie klasy potomne) implementuje interfejs Serializable, wszystkie wyjątki mogą być serializowane i deserializowane. Podczas deserializacji sprawdzana jest zgodność wersji klasy zserializowanej oraz klasy docelowej, do czego służy właśnie wspomniana stała.

Klasa XMLTools

Dla potrzeb funkcji pomocniczych, które mogą być wykorzystywane nie tylko podczas zapisu pliku XML, utworzyłem dodatkową klasę, zawierającą same metody statyczne:

package xml;

import java.lang.reflect.Annotatedattribute;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static xml.XMLException.*;

import xml.annotation.Attribute;
import xml.annotation.Contents;
import xml.annotation.attribute;
import xml.annotation.Text;

public class XMLTools {
}

Zacznę może od najprostrzych metod, czyli sprawdzających obecność adnotacji przy klasie danego obiektu:

public static boolean isattribute(Object object) {
    return object.getClass().isAnnotationPresent(attribute.class);
}

public static boolean isText(Object object) {
    return object.getClass().isAnnotationPresent(Text.class);
}

oraz przy składowej klasy (Field lub Method), za pośrednictwem interfejsu Annotatedattribute:

public static boolean isAttribute(Annotatedattribute annotated) {
    return annotated.isAnnotationPresent(Attribute.class);
}

public static boolean isContentsContainer(Annotatedattribute annotated) {
    return annotated.isAnnotationPresent(Contents.class);
}

public static boolean isattributeContent(Annotatedattribute annotated) {
    return annotated.isAnnotationPresent(attribute.class);
}

public static boolean isTextContent(Annotatedattribute annotated) {
    return annotated.isAnnotationPresent(Text.class);
}

public static boolean isContent(Annotatedattribute annotated) {
    return (
            isattributeContent(annotated) ||
            isTextContent(annotated) ||
            isContentsContainer(annotated));
}

W następnej kolejności zdefiniuję metodą pobierającą nazwę znacznika:

public static String getattributeTagName(Object attribute)
throws XMLException {
    if (!isattribute(attribute)) {
        throw new XMLException(String.format(
                ANNOTATION_NOT_FOUND, attribute.class.getName()));
    }
    String name =
        attribute.getClass().getAnnotation(attribute.class).value();
    if (name.length() == 0) {
        name = attribute.getClass().getSimpleName().toLowerCase();
    }
    return name;
}

Na początku należy sprawdzić, czy klasa obiektu oznaczona jest jako attribute XML. Wówczas można pobrać nazwę znacznika z adnotacji. Jeżeli ta nazwa jest pusta, należy pobrać nazwę klasy (po zamianie na małe litery).

Analogicznie wyglądają metody pobierające nazwę atrybutu:

public static String getAttributeName(Method attribute)
throws XMLException {
    if (!isAttribute(attribute)) {
        throw new XMLException(String.format(
                ANNOTATION_NOT_FOUND, Attribute.class.getName()));
    }
    String name = attribute.getAnnotation(Attribute.class).value();
    if (name.length() == 0) {
        name = attribute.getName().toLowerCase();
    }
    return name;
}

public static String getAttributeName(Field attribute)
throws XMLException {
    if (!isAttribute(attribute)) {
        throw new XMLException(String.format(
                ANNOTATION_NOT_FOUND, Attribute.class.getName()));
    }
    String name = attribute.getAnnotation(Attribute.class).value();
    if (name.length() == 0) {
        name = attribute.getName().toLowerCase();
    }
    return name;
}

Należy zauważyć, że ciała obu funkcji są identyczne. Obie klasy – Field oraz Method – implementują wykorzystywane przez powyższe metody interfejsy Annotatedattribute oraz Member. Jednak brakuje jednego interfejsu udostępniającego obie funkcjonalności. Dlatego można utworzyć dwie metody, albo jedną metodę z dwoma parametrami. Ten pierwszy sposób jest wygodniejszy w użyciu i poprawny.

Teraz zajmę się najbardziej skomplikowanym fragmentem kodu – pobieraniem kolekcji atrybutów, elementów czy tekstu. Każda z metod składa się z definicji kolekcji, dodania elementów na podstawie pól, a następnie metod, zwróceniu wyniku.

Najpierw pobranie mapy atrybutów elementu (mapa nie jest tak naprawdę kolekcją – to znaczy nie implementuje interfejsu Collection, jednak udostępnia kolekcje kluczy, wartości oraz wpisów):

public static Map<String, Object> getAttributes(
        Object element) throws XMLException {
    // Definicja mapy:
    Map<String, Object> attributes =
        new HashMap<String, Object>();
    // Pobranie atrybutów na podstawie pól:
    for (Field field : element.getClass().getFields()) {
        try {
            if (isAttribute(field) &&
                    (field.get(element) != null)) {
                attributes.put(
                        getAttributeName(field),
                        field.get(element));
            }
        } catch (IllegalAccessException e) {
            throw new XMLException(String.format(
                    MEMBER_ACCESS_DENIED,
                    element.getClass().getName(),
                    field.getName()));
        }
    }
    // Pobranie atrybutów na podstawie metod:
    for (Method method : element.getClass().getMethods()) {
        try {
            if (isAttribute(method) &&
                    (method.invoke(element) != null)) {
                attributes.put(
                        getAttributeName(method),
                        method.invoke(element));
            }
        } catch (IllegalArgumentException e) {
            throw new XMLException(String.format(
                    UNEXPECTED_ARGUMENTS,
                    element.getClass().getName(),
                    method.getName()));
        } catch (IllegalAccessException e) {
            throw new XMLException(String.format(
                    MEMBER_ACCESS_DENIED,
                    element.getClass().getName(),
                    method.getName()));
        } catch (InvocationTargetException e) {
            throw new XMLException(String.format(
                    INVOCATION_FAILED,
                    element.getClass().getName(),
                    method.getName()));
        }
    }
    // Zwrócenie wyniku:
    return attributes;
}

Dla każdego pola i każdej metody najpierw należy sprawdzić, czy udostępnia atrybut oraz czy jest on niepusty. Następnie prawidłowy atrybut jest dodawany do mapy, w postaci klucza (nazwy atrybutu) i wartości.

Podczas pobierania wartości pola (field.get(object), gdzie object – obiekt, z którego pobierana jest wartość pola) może zostać zgłoszony wyjątek IllegalAccessException. Oznacza to, że pole nie jest dostępne z miejsca wywołania. Jednak w powyższej sytuacji wyjątek taki nie wystąpi, ponieważ metoda getFields() pobiera wyłącznie pola publiczne.

W przypadku wywołania metody (method.invoke(object), gdzie object – obiekt, dla którego wykonywana jest metoda) okoliczności wystąpienia wyjątku IllegalAccessException są analogiczne. Jednak mogą wystąpić też dwa inne wyjątki:

IllegalArgumentException
oznacza, że argumenty występujące w metodzie nie zgadzają się z tymi w wywołaniu. W powyższej sytuacji wyjątek zostanie zgłoszony, jeżeli metoda posiada jakikolwiek argument.
InvocationTargetException
występuje wtedy, gdy wewnątrz wywołanej metody zostanie zgłoszony jakikolwiek wyjątek.

Teraz czas na definicję metody pobierającej listę fragmentów tekstu z obiektu klasy oznaczonej adnotacją @Text:

public static List<String> getTextContents(Object object)
throws XMLException {
    // Definicja listy:
    List<String> contents = new ArrayList<String>();
    // Pobranie tekstu na podstawie pól:
    for (Field field : object.getClass().getFields()) {
        try {
            if (isTextContent(field) &&
                    (field.get(object) != null))
                contents.add(String.valueOf(
                        field.get(object)));

        } catch (IllegalAccessException e) {
            throw new XMLException(String.format(
                    MEMBER_ACCESS_DENIED,
                    object.getClass().getName(),
                    field.getName()));
        }
    }
    // Pobranie tekstu na podstawie metod:
    for (Method method : object.getClass().getMethods()) {
        try {
            if (isTextContent(method) &&
                    (method.invoke(object) != null))
                contents.add(String.valueOf(
                        method.invoke(object)));

        } catch (IllegalArgumentException e) {
            throw new XMLException(String.format(
                    UNEXPECTED_ARGUMENTS,
                    object.getClass().getName(),
                    method.getName()));
        } catch (IllegalAccessException e) {
            throw new XMLException(String.format(
                    MEMBER_ACCESS_DENIED,
                    object.getClass().getName(),
                    method.getName()));
        } catch (InvocationTargetException e) {
            throw new XMLException(String.format(
                    INVOCATION_FAILED,
                    object.getClass().getName(),
                    method.getName()));
        }
    }
    // Zwrócenie wyniku:
    return contents;
}

Tym razem wartość pola lub metody zamieniana jest na łańcuch znaków.

Na koniec pozostawiłem metodę, która tworzy listę zawartości elementu. Pobierane będą wszystkie pola i metody oznaczone adnotacjami @Element, @Text albo @Contents:

public static List<Object> getContents(Object object) throws XMLException {
    // Definicja listy:
    List<Object> contents = new ArrayList<Object>();
    // Pobieranie zawartości na podstawie pól:
    for (Field field : object.getClass().getFields()) {
        if (isContent(field))
            try {
                Object value = field.get(object);
                addContentsToList(contents, field, value);

            } catch (IllegalAccessException e) {
                throw new XMLException(String.format(
                        MEMBER_ACCESS_DENIED,
                        object.getClass().getName(),
                        field.getName()));
            }
    }
    // Pobieranie zawartości na podstawie metod:
    for (Method method : object.getClass().getMethods()) {
        if (isContent(method))
            try {
                Object value = method.invoke(object);
                addContentsToList(contents, method, value);

            } catch (IllegalArgumentException e) {
                throw new XMLException(String.format(
                        UNEXPECTED_ARGUMENTS,
                        object.getClass().getName(),
                        method.getName()));
            } catch (IllegalAccessException e) {
                throw new XMLException(String.format(
                        MEMBER_ACCESS_DENIED,
                        object.getClass().getName(),
                        method.getName()));
            } catch (InvocationTargetException e) {
                throw new XMLException(String.format(
                        INVOCATION_FAILED,
                        object.getClass().getName(),
                        method.getName()));
            }
    }
    // Zwrócenie wyniku:
    return contents;
}

W powyższej metodzie, po sprawdzeniu, czy pole lub metoda posiada stosowną adnotację, pobierana jest wartość. Następnie wywoływana jest metoda pomocnicza:

private static void addContentsToList(
        List<Object> contents,
        AnnotatedElement annotated,
        Object value) {
    if (value != null) {
        if (isElementContent(annotated)) {
            contents.add(value);
        }
        if (isTextContent(annotated)) {
            contents.add(new SimpleText(String.valueOf(value)));
        }
        if (isContentsContainer(annotated)) {
            for (Object content : Iterable.class.cast(value))
                contents.add(content);
        }
    }
}

Metoda addContentsToList sprawdza, czy wartość nie jest pusta. Następnie – w zależności od adnotacji:

@Element
obiekt dodawany jest bezpośrednio do listy,
@Text
tworzony jest nowy obiekt typu SimpleText, który następnie zostaje dodany do listy,
@Contents
do listy dodawany jest każdy obiekt występujący w kolekcji.

Podsumowanie

Powyższy tekst opisuje techniczną stronę korzystania z adnotacji. Teraz dopiero widać, jaka funkcjonalność może kryć się pod paroma wyrazami dopisywanymi tu i ówdzie do kodu.

Następnym razem utworzę zawartość pliku XHTML i przetestuję jego poprawność.

Napisz Komentarz

*