Salta el contingut

7. Llibreries conversores

En aquesta unitat anem a estudiar algunes llibreries que ens permeten accelerar el procés de manipular els tipus de fitxers estudiats prèviament. Aquestes biblioteques faran servir classes serialitzadores i/o anotacions a les nostres classes per a definir la estrucutra dels fitxers a partir de les classes.

1. Jackson

1.1. Què és Jackson?

Ja hem vist que en algun moment hauràs de gestionar dades en format JSON (JavaScript Object Notation). Jackson és una llibreria de Java que simplifica enormement aquest procés, convertint-se en una eina pràcticament imprescindible en l'ecosistema Java modern.

Jackson fa una cosa molt concreta i la fa excepcionalment bé: la seriació i deseriatzació de dades entre objectes Java i el seu equivalent en format JSON.

  • Seriació (o Marshalling): Converteix un objecte Java (una instància d'una classe) en una cadena de text amb format JSON.

Deseriatzació (o Unmarshalling): El procés invers, converteix una cadena de text JSON en un objecte Java equivalent.

1.2. Què necessite?

Necessitem afegir la següent denpendència al nostre porjecte:

XML
1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.19.0</version> <!-- o la versió més actual -->
    </dependency>
</dependencies>

Jackson farà servir principalment les següents classes:

  • ObjectMapper: És la classe principal de Jackson i s'encarrega de la conversió entre objectes Java i JSON:
  • writeValue(): Mètode per convertir un objecte Java en JSON.
  • readValue(): Mètode per convertir JSON en un objecte Java.
  • JsonNode: Aquesta classe permet treballar amb arbres JSON en format node i és útil quan no coneixes l'estructura exacta del JSON.

1.3. Exemple

Si partim d'una classe o conjunt de classes sense cap modificació a la classe, ens farà un objecte JSNO com segueix

Java
1
2
3
4
5
6
7
8
public class Llibre {
    private int id;
    private String titol;
    private String genere;

   // Constructors habitual

    // Getters i setters
}

Java
// Creem l'ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();

// Convertim l'objecte a JSON
String json = null;
try {
    // string a pelo, sin formato
    json = objectMapper.writeValueAsString(llibre);
    System.out.println(json);
    // així millor
    json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(llibre);
    System.out.println(json);
} // falta la captura d'excepcions
JSON
1
2
3
4
5
6
{"id":1,"titol":"Quijote","genere":"Caballeria"}
{
"id" : 1,
"titol" : "Quijote",
"genere" : "Caballeria"
}

Com podem veure el resultat és molt acceptable

I un Array o objectes amb arrays
Java
Llibre llibre1 = new Llibre(1, "La casa de los espíritus", "Realista");
Llibre llibre2 = new Llibre(2, "Cien años de soledad", "Realista");
Llibre llibre3 = new Llibre(3, "El amor en los tiempos del cólera", "Romàntic");
Llibre llibre4 = new Llibre(4, "La sombra del viento", "Misteri");
Llibre llibre5 = new Llibre(5, "El juego del ángel", "Misteri");
Llibre llibre6 = new Llibre(6, "Kafka en la orilla", "Filosòfic");
Llibre llibre7 = new Llibre(7, "Norwegian Wood", "Drama");
Llibre llibre8 = new Llibre(8, "1Q84", "Ciència ficció");

// Creem 5 autors
Autor autor1 = new Autor(1, "Isabel Allende", "Xile", Arrays.asList(llibre1, llibre2));
Autor autor2 = new Autor(2, "Gabriel García Márquez", "Colòmbia", Arrays.asList(llibre3));
Autor autor3 = new Autor(3, "Carlos Ruiz Zafón", "Espanya", Arrays.asList(llibre4, llibre5));
Autor autor4 = new Autor(4, "Haruki Murakami", "Japó", Arrays.asList(llibre6, llibre7, llibre8));
Autor autor5 = new Autor(5, "Elena Ferrante", "Itàlia", Arrays.asList());

// Imprimim els autors
autors = Arrays.asList(autor1, autor2, autor3, autor4, autor5);

llibres=Arrays.asList(llibre1, llibre2, llibre3, llibre4, llibre5, llibre6, llibre7, llibre8);
JSON
[ {
    "id" : 1,
    "titol" : "La casa de los espíritus",
    "genere" : "Realista"
    }, {
    "id" : 2,
    "titol" : "Cien años de soledad",
    "genere" : "Realista"
    }, {
    "id" : 3,
    "titol" : "El amor en los tiempos del cólera",
    "genere" : "Romàntic"
    }, {
    "id" : 4,
    "titol" : "La sombra del viento",
    "genere" : "Misteri"
    }, {
    "id" : 5,
    "titol" : "El juego del ángel",
    "genere" : "Misteri"
    }, {
    "id" : 6,
    "titol" : "Kafka en la orilla",
    "genere" : "Filosòfic"
    }, {
    "id" : 7,
    "titol" : "Norwegian Wood",
    "genere" : "Drama"
    }, {
    "id" : 8,
    "titol" : "1Q84",
    "genere" : "Ciència ficció"
    } ]
JSON
    [ {
        "id" : 1,
        "nom" : "Isabel Allende",
        "nacionalitat" : "Xile",
        "llibres" : [ {
            "id" : 1,
            "titol" : "La casa de los espíritus",
            "genere" : "Realista"
        }, {
            "id" : 2,
            "titol" : "Cien años de soledad",
            "genere" : "Realista"
        } ]
        }, {
        "id" : 2,
        "nom" : "Gabriel García Márquez",
        "nacionalitat" : "Colòmbia",
        "llibres" : [ {
            "id" : 3,
            "titol" : "El amor en los tiempos del cólera",
            "genere" : "Romàntic"
        } ]
        }, {
        "id" : 3,
        "nom" : "Carlos Ruiz Zafón",
        "nacionalitat" : "Espanya",
        "llibres" : [ {
            "id" : 4,
            "titol" : "La sombra del viento",
            "genere" : "Misteri"
        }, {
            "id" : 5,
            "titol" : "El juego del ángel",
            "genere" : "Misteri"
        } ]
        }, {
        "id" : 4,
        "nom" : "Haruki Murakami",
        "nacionalitat" : "Japó",
        "llibres" : [ {
            "id" : 6,
            "titol" : "Kafka en la orilla",
            "genere" : "Filosòfic"
        }, {
            "id" : 7,
            "titol" : "Norwegian Wood",
            "genere" : "Drama"
        }, {
            "id" : 8,
            "titol" : "1Q84",
            "genere" : "Ciència ficció"
        } ]
        }, {
        "id" : 5,
        "nom" : "Elena Ferrante",
        "nacionalitat" : "Itàlia",
        "llibres" : [ ]
        } ]

1.4. Modificant algunes propietats del json

Jackson ofereix un ampli conjunt d'anotacions per a personalitzar el procés de conversió sense haver de modificar la lògica del codi. Algunes bàsiques son:

  • @JsonProperty pots canviar el nom d'un camp en el JSON
  • @JsonIgnore pots ometre un atribut, i amb
  • @JsonFormat pots definir el format d'una data.

Exemple:

Java
public class Llibre2 {

    // Canviem "id" per "isbn"
    @JsonProperty("isbn")  // Opcional, ja que el nom de l'atribut coincideix amb el nom del JSON
    private int isbn;

    @JsonProperty("titol_llibre")  // Canvia el nom de "titol" a "titol_llibre" al JSON
    private String titol;

    private String genere;

    @JsonIgnore  // Omiteix el camp "notaInterna" a l'hora de convertir a JSON
    private String notaInterna;

    @JsonFormat(pattern = "dd-MM-yyyy")  // Personalitza el format de la data
    private Date dataPublicacio;

    // Constructor, getters i settes
}

1.5. Avancem continugt

Spring, el workbench que estudiarem a l'unitat 6, fa servir internament Jackson per a seriar els objectes i retornar-los mitjançant la seua API. De vegades ens podrem trobar problemes quan tenim objectes doblement enllaçats, com per exemple un Jugador que ha pertany un Equip on dins dels Equips tenim guardat els seus Jugador.

Aquest problema no és nou, ja que por apareixer també al generar el propi toString i l'aparicicó del famós missatge StackOverflowException degut a la recursió infinita d'anar d'un objecte a altre.

Per a evitar-ho amb la serialització amb JSON, disposem de @JsonManagedReference i @JsonBackReference.

  • @JsonManagedReference: Es col·loca a la propietat que s'ha de serialitzar normalment (el "pare" o el costat "cap avant" de la relació). En aquesta classe volem que aparega les propietats. Al nostre exemple, seria la llista de Jugadors dins de la classe Equip.
  • @JsonBackReference: Es col·loca a la propietat que causa el bucle i que no es tornarà a serialitzar (el "fill" o el costat "cap enrere"). En aquest cas, seria la propietat Equip dins de la classe Jugador. Això el que provoca també és que no es mostre l'equip al convertir el jugador
Java
public class Equip {

    public long id;
    public String nom;

    @JsonManagedReference
    public List<Jugador> jugadors;

    // Constructors Getters i Setters (omesos per brevetat)
}
Java
public class Jugador {

public long id;
public String nom;
public int dorsal;

@JsonBackReference()
public Equip equip;

// Constructors Getters i Setters
Java
    public void testReferencies(){
    // 1. Creem l'equip
    Equip valenciaCF = new Equip(1, "València CF");

    // 2. Creem els jugadors
    Jugador jugador1 = new Jugador(10, "Pepelu", 18);
    Jugador jugador2 = new Jugador(20, "Hugo Duro", 9);
    Jugador jugador3 = new Jugador(25, "Giorgi Mamardashvili", 1);

    // 3. Afegim els jugadors a l'equip (establint la relació bidireccional)
    valenciaCF.afegirJugador(jugador1);
    valenciaCF.afegirJugador(jugador2);
    valenciaCF.afegirJugador(jugador3);

    // 4. Mostrem l'equip (i els seus jugador)
    sampleObjectToJson(valenciaCF);

    // 5. Mostrem un jugador (però no l'equip)
    sampleObjectToJson(jugador1);
}
JSON
{
    "id" : 1,
    "nom" : "València CF",
    "jugadors" : [ {
        "id" : 10,
        "nom" : "Pepelu",
        "dorsal" : 18
    }, {
        "id" : 20,
        "nom" : "Hugo Duro",
        "dorsal" : 9
    }, {
        "id" : 25,
        "nom" : "Giorgi Mamardashvili",
        "dorsal" : 1
    } ]
}
{
    "id" : 10,
    "nom" : "Pepelu",
    "dorsal" : 18
}

Investiga

Prova a buscar una solució més completa amb @JsonIdentityInfo

2. JAXB

Per processar XML, saps que amb DOM, has de recórrer arbres de nodes (NodeList, Element, Attr). JAXB (Java Architecture for XML Binding) representa un canvi de paradigma. En lloc de veure l'XML com un arbre de nodes, JAXB el tracta com una font de dades per a objectes Java. La seva funció principal és actuar com un pont automàtic entre el món dels documents XML i el món dels teus objectes de domini (POJOs - Plain Old Java Objects). - Unmarshalling: És el procés de llegir un document XML i convertir-lo automàticament en una estructura d'objectes Java. JAXB llegeix l'XML, instancia les classes corresponents i assigna els valors als seus camps. - Marshalling: És el procés invers. Agafa un objecte Java i el converteix en la seva representació XML equivalent.

De manera molt similar a com la llibreria Jackson ho fa amb JSON, JAXB utilitza anotacions per mapejar les classes i els camps de Java amb els elements i atributs de l'XML.

Primerament caldrà afegir la dependència de JABX al POM (en projectes maven)

XML
    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
        <version>4.0.2</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>4.0.5</version>
    </dependency>

2.1. Anotacions a nivell general o de classe

  • @XmlRootElement → És l'anotació més important. Indica que aquesta classe pot ser l'element arrel d'un document XML. Sense ella, JAXB no sabria per on començar a l'hora de fer marshalling d'un objecte.
  • @XmlAccessorType → Defineix quins camps de la classe s'han d'incloure en el procés de binding per defecte. L'opció més comuna és XmlAccessType.FIELD
  • @XmlType → Permet definir l'ordre en què apareixeran els elements dins de l'XML generat, la qual cosa és molt útil per crear documents amb una estructura predefinida i consistent. @XmlType(propOrder = { "nom", "dorsal", "demarcacio" })

2.2. Anotacions a nivell de atribut

  • @XmlElement → indica que és un element XML
  • @XmlAttribute → indica que serà com un atribut
  • @XmlElementWrapper → indica que aquesta llista és una colecció d'elements
  • @XmlTransient → indica que aquest atribut no es processa (com el JSONIgnore)

2.3. I el problema de la recursió

La solució per a la recursivitat en JAXB es fa normalment amb les anotacions @XmlID i @XmlIDREF. Vegem l'exemple amb Jugador i Equip

  • @XmlID: Es posa en un camp que actua com a identificador únic (i ha de ser de tipus String). Aquest serà l'id del nostre Equip.
  • @XmlIDREF: Es posa en el camp que fa la referència de tornada (el camp equip dins de Jugador). En lloc d'incrustar tot l'objecte, JAXB només escriurà el valor de l'identificador (@XmlID) de l'objecte referenciat.

2.4. Exemple

Java
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"id","nom","equip"}) // Defineix l'ordre dels elements interns
public class Jugador {

    @XmlElement
    public long id;

    @XmlElement
    public String nom;

    @XmlAttribute
    public int dorsal;

    @JsonBackReference()
    @XmlIDREF   // <-- Aquesta és la clau per evitar la recursivitat!
    public Equip equip;

    // Constructors
    public Jugador() {}

    public Jugador(long id, String nom, int dorsal) {
        this.id = id;
        this.nom = nom;
        this.dorsal = dorsal;
    }

    // Getters i Setters
    public void setEquip(Equip equip) {
        this.equip = equip;
    }

    // La resta de getters i setters s'ometen per brevetat
}
Java
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.xml.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@XmlRootElement(name = "equip")
@XmlAccessorType(XmlAccessType.FIELD) // Processa els camps directament
public class Equip {

    @XmlAttribute // L'ID de l'equip serà un atribut
    @XmlID      // Aquest és l'identificador únic per a JAXB
    public String id; // Canviat a String per complir amb @XmlID que requereix String

    @XmlElement
    public String nom;

    @JsonManagedReference
    @XmlElementWrapper(name = "elsJjugadors") // Etiqueta que envoltarà la llista
    @XmlElement(name = "jugador")      // Nom per a cada element de la llista
    public List<Jugador> jugadors;

    // Constructors
    public Equip() {
        this.jugadors = new ArrayList<>();
    }

    public Equip(String id, String nom) {
        this.id = id;
        this.nom = nom;
        this.jugadors = new ArrayList<>();
    }

    // Mètode d'ajuda per afegir jugadors i establir la relació
    public void afegirJugador(Jugador jugador) {
        this.jugadors.add(jugador);
        jugador.setEquip(this);
    }

    // Getters i Setters (omesos per brevetat)
}
Java
public  void testXML(String Filename){

    try{
            // 1. Creem l'equip i els jugadors
            Equip valenciaCF = new Equip("Valencia2025", "València CF");

            Jugador j1 = new Jugador(10, "Pepelu", 18);
            Jugador j2 = new Jugador(20, "Hugo Duro", 9);

            // 2. Vinculem els jugadors a l'equip
            valenciaCF.afegirJugador(j1);
            valenciaCF.afegirJugador(j2);

            // 3. Creem el context JAXB per a les classes que utilitzarem
            JAXBContext context = JAXBContext.newInstance(Equip.class);

            // 4. Creem el Marshaller
            Marshaller marshaller = context.createMarshaller();

            // Configurem la propietat per a una sortida XML formatada (indentada)
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

            // 5. Convertim l'objecte a XML i ho mostrem per la sortida estàndard
            System.out.println("--- Resultat XML Generat ---");
            marshaller.marshal(valenciaCF, System.out);

            // 6. També podem guardar el XML en un fitxer
            if (Filename!=null && !Filename.isEmpty()){
                marshaller.marshal(valenciaCF, new java.io.File(Filename));
            }
        }
    catch (JAXBException e) {
        throw new RuntimeException(e);
    }

}
XML
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<equip id="Valencia2025">
    <nom>València CF</nom>
    <elsJjugadors>
        <jugador dorsal="18">
            <id>10</id>
            <nom>Pepelu</nom>
            <equip>Valencia2025</equip>
        </jugador>
        <jugador dorsal="9">
            <id>20</id>
            <nom>Hugo Duro</nom>
            <equip>Valencia2025</equip>
        </jugador>
    </elsJjugadors>
</equip>

3. Commons CSV

Finalment el format CSV el veurem sols mitjançant un exemple amb la llibreria Commons CSV. Caldrà afegir:

XML
1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.11.0</version>
</dependency>
Text Only
titol,director,any,generes
Inception,Christopher Nolan,2010,Sci-Fi
The Dark Knight,Christopher Nolan,2008,Action
Interstellar,Christopher Nolan,2014,Sci-Fi
,Bong Joon-ho,2019,Thriller
Joker,Todd Phillips,2019,Crime
Avengers: Endgame,Anthony Russo,2019,Action
1917,Sam Mendes,2019,War
Tenet,,2020,Sci-Fi
Soul,Pete Docter,2020,Animation
Knives Out,Rian Johnson,2019,Mystery
Jojo Rabbit,Taika Waititi,2019,Comedy
Ford v Ferrari,James Mangold,2019,Drama
The Irishman,Martin Scorsese,,Crime
Once Upon a Time in Hollywood,Quentin Tarantino,2019,Drama
Little Women,Greta Gerwig,2019,Drama
Marriage Story,Noah Baumbach,2019,Drama
La La Land,Damien Chazelle,2016,Musical
Dunkirk,Christopher Nolan,2017,War
Blade Runner 2049,Denis Villeneuve,2017,Sci-Fi
Arrival,Denis Villeneuve,2016,Sci-Fi
Java
// Classe Pel·lícula
public class Pelicula {
    private String titol;
    private String director;
    private int any;
    private String genere;

    public Pelicula(String titol, String director, int any, String genere) {
        this.titol = titol;
        this.director = director;
        this.any = any;
        this.genere = genere;
    }

    @Override
    public String toString() {
        return "Pelicula{" +
                "titol='" + titol + '\'' +
                ", director='" + director + '\'' +
                ", any=" + any +
                ", genere='" + genere + '\'' +
                '}';
    }
}
Java
public void testCommonCSV(){
    String csvFile = "peliculas.csv";  // Ruta del CSV
    List<Pelicula> pelicules = new ArrayList<>();
    int numRecords=0;
    try{
        FileReader reader = new FileReader(csvFile);

        CSVFormat format = CSVFormat.DEFAULT.builder()
                .setHeader()
                .setSkipHeaderRecord(true)
                .build();

        CSVParser csvParser = new CSVParser(reader, format);

        for (CSVRecord record : csvParser) {
            numRecords++;
            String titol = record.get("titol");
            String director = record.get("director");
            String anyStr = record.get("any");
            String genere = record.get("generes");

            // Comprovem si alguna dada és nula o buida
            if (titol == null || titol.isEmpty() ||
                director == null || director.isEmpty() ||
                anyStr == null || anyStr.isEmpty() ||
                genere == null || genere.isEmpty()) {
                continue;  // Saltar aquesta pel·lícula
            }

            int any = Integer.parseInt(anyStr);
            Pelicula pelicula = new Pelicula(titol, director, any, genere);
            pelicules.add(pelicula);
        }
    } catch (FileNotFoundException ex) {
        throw new RuntimeException(ex);
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }

    System.out.print(Colors.green+"S'han llegit "+numRecords+" registres"+Colors.reset);
    System.out.println(Colors.yellow+". Però finalment llegides " + pelicules.size()+ Colors.reset);
    // Mostrem totes les pel·lícules carregades
    for (Pelicula p : pelicules) {
        System.out.println(p);
    }
}
Text Only
S'han llegit 20 registresPerò finalment llegides 17
Pelicula{titol='Inception', director='Christopher Nolan', any=2010, genere='Sci-Fi'}
Pelicula{titol='The Dark Knight', director='Christopher Nolan', any=2008, genere='Action'}
Pelicula{titol='Interstellar', director='Christopher Nolan', any=2014, genere='Sci-Fi'}
Pelicula{titol='Joker', director='Todd Phillips', any=2019, genere='Crime'}
Pelicula{titol='Avengers: Endgame', director='Anthony Russo', any=2019, genere='Action'}
Pelicula{titol='1917', director='Sam Mendes', any=2019, genere='War'}
Pelicula{titol='Soul', director='Pete Docter', any=2020, genere='Animation'}
Pelicula{titol='Knives Out', director='Rian Johnson', any=2019, genere='Mystery'}
Pelicula{titol='Jojo Rabbit', director='Taika Waititi', any=2019, genere='Comedy'}
Pelicula{titol='Ford v Ferrari', director='James Mangold', any=2019, genere='Drama'}
Pelicula{titol='Once Upon a Time in Hollywood', director='Quentin Tarantino', any=2019, genere='Drama'}
Pelicula{titol='Little Women', director='Greta Gerwig', any=2019, genere='Drama'}
Pelicula{titol='Marriage Story', director='Noah Baumbach', any=2019, genere='Drama'}
Pelicula{titol='La La Land', director='Damien Chazelle', any=2016, genere='Musical'}
Pelicula{titol='Dunkirk', director='Christopher Nolan', any=2017, genere='War'}
Pelicula{titol='Blade Runner 2049', director='Denis Villeneuve', any=2017, genere='Sci-Fi'}
Pelicula{titol='Arrival', director='Denis Villeneuve', any=2016, genere='Sci-Fi'}