web-dev-qa-db-fra.com

JAXB: comment mapper dans <key> value </ key>

La question porte sur la gestion des cartes JAXB - il existe de nombreux exemples sur la manière de convertir une carte en une structure comme suit:

<map>
  <entry>
    <key> KEY </key>
    <value> VALUE </value>
  </entry>
  <entry>
    <key> KEY2 </key>
    <value> VALUE2 </value>
  </entry>
  <entry>
  ...
</map>

En fait, cela est supporté nativement par JAXB. Ce dont j'ai besoin, cependant, c'est du XML où clé est le nom de l'élément et la valeur, son contenu:

<map>
  <key> VALUE </key>
  <key2> VALUE2 </key2>
 ...
</map>

Je n’ai pas réussi à implémenter mon adaptateur de carte comme recommandé par les développeurs JAXB ( https://jaxb.dev.Java.net/guide/Mapping_your_favorite_class.html ), car j’ai besoin, il - nom d'attribut dynamique :)

Y a-t-il une solution pour ça?

P.S. Actuellement, je dois créer une classe de conteneur dédiée pour chaque ensemble typique de paires clé-valeur que je souhaite transférer en XML - cela fonctionne, mais je dois créer beaucoup trop de ces conteneurs auxiliaires.

64
Timur

le code fourni n'a pas fonctionné pour moi. J'ai trouvé un autre moyen de mapper:

MapElements:

package com.cellfish.mediadb.rest.lucene;

import javax.xml.bind.annotation.XmlElement;

class MapElements
{
  @XmlElement public String  key;
  @XmlElement public Integer value;

  private MapElements() {} //Required by JAXB

  public MapElements(String key, Integer value)
  {
    this.key   = key;
    this.value = value;
  }
}

MapAdapter:

import Java.util.HashMap;
import Java.util.Map;

import javax.xml.bind.annotation.adapters.XmlAdapter;

class MapAdapter extends XmlAdapter<MapElements[], Map<String, Integer>> {
    public MapElements[] marshal(Map<String, Integer> arg0) throws Exception {
        MapElements[] mapElements = new MapElements[arg0.size()];
        int i = 0;
        for (Map.Entry<String, Integer> entry : arg0.entrySet())
            mapElements[i++] = new MapElements(entry.getKey(), entry.getValue());

        return mapElements;
    }

    public Map<String, Integer> unmarshal(MapElements[] arg0) throws Exception {
        Map<String, Integer> r = new HashMap<String, Integer>();
        for (MapElements mapelement : arg0)
            r.put(mapelement.key, mapelement.value);
        return r;
    }
}

Le rootElement:

import Java.util.HashMap;
import Java.util.Map;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
public class Root {

    private Map<String, Integer> mapProperty;

    public Root() {
        mapProperty = new HashMap<String, Integer>();
    }

    @XmlJavaTypeAdapter(MapAdapter.class)
    public Map<String, Integer> getMapProperty() {
        return mapProperty;
    }

    public void setMapProperty(Map<String, Integer> map) {
        this.mapProperty = map;
    }

}

J'ai trouvé le code sur ce site: http://www.developpez.net/forums/d972324/Java/general-Java/xml/hashmap-jaxb/

21
Grégory

Cela peut être une raison valable pour laquelle vous souhaitez procéder, mais il est généralement préférable d'éviter de générer ce type de XML. Pourquoi? Parce que cela signifie que les éléments XML de votre carte dépendent du contenu d'exécution de votre carte. Et comme XML est généralement utilisé comme interface externe ou couche d’interface, cela n’est pas souhaitable. Laissez-moi expliquer.

Le schéma XML (xsd) définit le contrat d'interface de vos documents XML. En plus de pouvoir générer du code à partir de XSD, JAXB peut également générer le schéma XML pour vous à partir du code. Cela vous permet de limiter les données échangées via l'interface aux structures préalablement convenues définies dans le XSD.

Dans le cas par défaut pour un Map<String, String>, la XSD générée restreindra l’élément de la carte à plusieurs éléments d’entrée dont chacun doit contenir un xs:string clé et un xs:string valeur. C'est un contrat d'interface assez clair.

Ce que vous décrivez, c'est que vous voulez que la carte XML contienne des éléments dont le nom sera déterminé par le contenu de la carte à l'exécution. Ensuite, le fichier XSD généré peut uniquement spécifier que la carte doit contenir une liste d'éléments dont le type est inconnu au moment de la compilation. Vous devez généralement éviter cela lors de la définition d'un contrat d'interface.

Pour obtenir un contrat strict dans ce cas, vous devez utiliser un type énuméré comme clé de la carte au lieu d'une chaîne. Par exemple.

public enum KeyType {
 KEY, KEY2;
}

@XmlJavaTypeAdapter(MapAdapter.class)
Map<KeyType , String> mapProperty;

De cette manière, les clés dont vous voulez faire partie en XML sont connues au moment de la compilation. JAXB devrait donc être en mesure de générer un schéma qui restreindrait les éléments de la carte aux éléments en utilisant l’une des clés prédéfinies KEY ou KEY2.

D'autre part, si vous souhaitez simplifier la structure générée par défaut

<map>
    <entry>
        <key>KEY</key>
        <value>VALUE</value>
    </entry>
    <entry>
        <key>KEY2</key>
        <value>VALUE2</value>
    </entry>
</map>

Pour quelque chose de plus simple comme ça

<map>
    <item key="KEY" value="VALUE"/>
    <item key="KEY2" value="VALUE2"/>
</map>

Vous pouvez utiliser un MapAdapter qui convertit la carte en un tableau de MapElements comme suit:

class MapElements {
    @XmlAttribute
    public String key;
    @XmlAttribute
    public String value;

    private MapElements() {
    } //Required by JAXB

    public MapElements(String key, String value) {
        this.key = key;
        this.value = value;
    }
}


public class MapAdapter extends XmlAdapter<MapElements[], Map<String, String>> {
    public MapAdapter() {
    }

    public MapElements[] marshal(Map<String, String> arg0) throws Exception {
        MapElements[] mapElements = new MapElements[arg0.size()];
        int i = 0;
        for (Map.Entry<String, String> entry : arg0.entrySet())
            mapElements[i++] = new MapElements(entry.getKey(), entry.getValue());

        return mapElements;
    }

    public Map<String, String> unmarshal(MapElements[] arg0) throws Exception {
        Map<String, String> r = new TreeMap<String, String>();
        for (MapElements mapelement : arg0)
            r.put(mapelement.key, mapelement.value);
        return r;
    }
}
27
Justin Rowe

Je travaille toujours sur une meilleure solution, mais avec MOXy JAXB , j'ai été en mesure de gérer le code XML suivant:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <mapProperty>
      <map>
         <key>value</key>
         <key2>value2</key2>
      </map>
   </mapProperty>
</root>

Vous devez utiliser un @XmlJavaTypeAdapter sur votre propriété Map:

import Java.util.HashMap;
import Java.util.Map;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
public class Root {

    private Map<String, String> mapProperty;

    public Root() {
        mapProperty = new HashMap<String, String>();
    }

    @XmlJavaTypeAdapter(MapAdapter.class)
    public Map<String, String> getMapProperty() {
        return mapProperty;
    }

    public void setMapProperty(Map<String, String> map) {
        this.mapProperty = map;
    }

}

L'implémentation de XmlAdapter est la suivante:

import Java.util.HashMap;
import Java.util.Map;
import Java.util.Map.Entry;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class MapAdapter extends XmlAdapter<AdaptedMap, Map<String, String>> {

    @Override
    public AdaptedMap marshal(Map<String, String> map) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        Element rootElement = document.createElement("map");
        document.appendChild(rootElement);

        for(Entry<String,String> entry : map.entrySet()) {
            Element mapElement = document.createElement(entry.getKey());
            mapElement.setTextContent(entry.getValue());
            rootElement.appendChild(mapElement);
        }

        AdaptedMap adaptedMap = new AdaptedMap();
        adaptedMap.setValue(document);
        return adaptedMap;
    }

    @Override
    public Map<String, String> unmarshal(AdaptedMap adaptedMap) throws Exception {
        Map<String, String> map = new HashMap<String, String>();
        Element rootElement = (Element) adaptedMap.getValue();
        NodeList childNodes = rootElement.getChildNodes();
        for(int x=0,size=childNodes.getLength(); x<size; x++) {
            Node childNode = childNodes.item(x);
            if(childNode.getNodeType() == Node.ELEMENT_NODE) {
                map.put(childNode.getLocalName(), childNode.getTextContent());
            }
        }
        return map;
    }

}

La classe AdpatedMap est l'endroit où toute la magie se produit, nous allons utiliser un DOM pour représenter le contenu. Nous allons tromper l'intro de JAXB face à un DOM grâce à la combinaison de @XmlAnyElement et d'une propriété de type Object:

import javax.xml.bind.annotation.XmlAnyElement;

public class AdaptedMap {

    private Object value;

    @XmlAnyElement
    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

}

Cette solution nécessite l'implémentation MOXy JAXB. Vous pouvez configurer le runtime JAXB pour utiliser l'implémentation MOXy en ajoutant un fichier nommé jaxb.properties dans vos classes de modèle avec l'entrée suivante:

javax.xml.bind.context.factory=org.Eclipse.persistence.jaxb.JAXBContextFactory

Le code de démonstration suivant peut être utilisé pour vérifier le code:

import Java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Root root = (Root) unmarshaller.unmarshal(new File("src/forum74/input.xml"));

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }
}
15
bdoughan

Je n'ai rien vu qui réponde vraiment très bien à cette question. J'ai trouvé quelque chose qui a très bien fonctionné ici:

tilisez le type de style JAXB XMLAnyElement pour renvoyer les noms d'élément dynamique

Je l'ai un peu modifié pour prendre en charge les arbres hashmap. Vous pouvez ajouter d'autres collections.

public class MapAdapter extends XmlAdapter<MapWrapper, Map<String, Object>> {
    @Override
    public MapWrapper marshal(Map<String, Object> m) throws Exception {
        MapWrapper wrapper = new MapWrapper();
        List elements = new ArrayList();
        for (Map.Entry<String, Object> property : m.entrySet()) {

            if (property.getValue() instanceof Map)
                elements.add(new JAXBElement<MapWrapper>(new QName(getCleanLabel(property.getKey())),
                        MapWrapper.class, marshal((Map) property.getValue())));
            else
                elements.add(new JAXBElement<String>(new QName(getCleanLabel(property.getKey())),
                        String.class, property.getValue().toString()));
        }
        wrapper.elements = elements;
        return wrapper;
    }

    @Override
    public Map<String, Object> unmarshal(MapWrapper v) throws Exception {
        // TODO
        throw new OperationNotSupportedException();
    }

    // Return a XML-safe attribute.  Might want to add camel case support
    private String getCleanLabel(String attributeLabel) {
        attributeLabel = attributeLabel.replaceAll("[()]", "").replaceAll("[^\\w\\s]", "_").replaceAll(" ", "_");
        return attributeLabel;
    }
}
class MapWrapper {
    @XmlAnyElement
    List elements;
}

Puis pour le mettre en oeuvre:

static class myxml {
    String name = "Full Name";
    String address = "1234 Main St";
    // I assign values to the map elsewhere, but it's just a simple
    // hashmap with a hashmap child as an example.
    @XmlJavaTypeAdapter(MapAdapter.class)
    public Map<String, Object> childMap;
}

Nourrir cela par un simple Marshaller donne une sortie qui ressemble à ceci:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<myxml>
    <name>Full Name</name>
    <address>1234 Main St</address>
    <childMap>
        <key2>value2</key2>
        <key1>value1</key1>
        <childTree>
            <childkey1>childvalue1</childkey1>
        </childTree>
    </childMap>
</myxml>
10
JavaJeff

(Désolé, impossible d'ajouter des commentaires)

Dans la réponse de Blaise ci-dessus, si vous changez:

@XmlJavaTypeAdapter(MapAdapter.class)
public Map<String, String> getMapProperty() {
    return mapProperty;
}

à:

@XmlJavaTypeAdapter(MapAdapter.class)
@XmlPath(".") // <<-- add this
public Map<String, String> getMapProperty() {
    return mapProperty;
}

alors cela devrait se débarrasser de la <mapProperty> tag, et ainsi vous donner:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <map>
        <key>value</key>
        <key2>value2</key2>
    </map>
</root>

ALTERNATIVEMENT:

Vous pouvez également le changer pour:

@XmlJavaTypeAdapter(MapAdapter.class)
@XmlAnyElement // <<-- add this
public Map<String, String> getMapProperty() {
    return mapProperty;
}

et alors vous pouvez vous débarrasser complètement de AdaptedMap, et changer simplement MapAdapter en marshall en un objet Document directement. Je n'ai testé cela qu'avec le marshalling, il peut donc y avoir des problèmes incontrôlables.

Je vais essayer de trouver le temps de simuler un exemple complet et de modifier ce message en conséquence.

3
lukens

J'ai la solution sans adaptateur. Carte transitoire convertie en éléments xml et inversement:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "SchemaBasedProperties")
public class SchemaBasedProperties
{
  @XmlTransient
  Map<String, Map<String, String>> properties;

  @XmlAnyElement(lax = true)
  List<Object> xmlmap;

  public Map<String, Map<String, String>> getProperties()
  {
    if (properties == null)
      properties = new LinkedHashMap<String, Map<String, String>>(); // I want same order

    return properties;
  }

  boolean beforeMarshal(Marshaller m)
  {
    try
    {
      if (properties != null && !properties.isEmpty())
      {
        if (xmlmap == null)
          xmlmap = new ArrayList<Object>();
        else
          xmlmap.clear();

        javax.xml.parsers.DocumentBuilderFactory dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance();
        javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
        org.w3c.dom.Document doc = db.newDocument();
        org.w3c.dom.Element element;

        Map<String, String> attrs;

        for (Map.Entry<String, Map<String, String>> it: properties.entrySet())
        {
          element = doc.createElement(it.getKey());
          attrs = it.getValue();

          if (attrs != null)
            for (Map.Entry<String, String> at: attrs.entrySet())
              element.setAttribute(at.getKey(), at.getValue());

          xmlmap.add(element);
        }
      }
      else
        xmlmap = null;
    }
    catch (Exception e)
    {
      e.printStackTrace();
      return false;
    }

    return true;
  }

  void afterUnmarshal(Unmarshaller u, Object p)
  {
    org.w3c.dom.Node node;
    org.w3c.dom.NamedNodeMap nodeMap;

    String name;
    Map<String, String> attrs;

    getProperties().clear();

    if (xmlmap != null)
      for (Object xmlNode: xmlmap)
        if (xmlNode instanceof org.w3c.dom.Node)
        {
          node = (org.w3c.dom.Node) xmlNode;
          nodeMap = node.getAttributes();

          name = node.getLocalName();
          attrs = new HashMap<String, String>();

          for (int i = 0, l = nodeMap.getLength(); i < l; i++)
          {
            node = nodeMap.item(i);
            attrs.put(node.getNodeName(), node.getNodeValue());
          }

          getProperties().put(name, attrs);
        }

    xmlmap = null;
  }

  public static void main(String[] args)
    throws Exception
  {
    SchemaBasedProperties props = new SchemaBasedProperties();
    Map<String, String> attrs;

    attrs = new HashMap<String, String>();
    attrs.put("ResId", "A_LABEL");
    props.getProperties().put("LABEL", attrs);

    attrs = new HashMap<String, String>();
    attrs.put("ResId", "A_TOOLTIP");
    props.getProperties().put("TOOLTIP", attrs);

    attrs = new HashMap<String, String>();
    attrs.put("Value", "hide");
    props.getProperties().put("DISPLAYHINT", attrs);

    javax.xml.bind.JAXBContext jc = javax.xml.bind.JAXBContext.newInstance(SchemaBasedProperties.class);

    Marshaller marshaller = jc.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(props, new Java.io.File("test.xml"));

    Unmarshaller unmarshaller = jc.createUnmarshaller();
    props = (SchemaBasedProperties) unmarshaller.unmarshal(new Java.io.File("test.xml"));

    System.out.println(props.getProperties());
  }
}

Ma sortie comme prévu:

<SchemaBasedProperties>
    <LABEL ResId="A_LABEL"/>
    <TOOLTIP ResId="A_TOOLTIP"/>
    <DISPLAYHINT Value="hide"/>
</SchemaBasedProperties>

{LABEL={ResId=A_LABEL}, TOOLTIP={ResId=A_TOOLTIP}, DISPLAYHINT={Value=hide}}

Vous pouvez utiliser une paire nom/valeur d'élément. J'ai besoin d'attributs ... Amusez-vous!

2
lunicon

On dirait que cette question est en quelque sorte une duplication avec une autre, où j'ai rassemblé quelques solutions marshal/unmarshal dans un poste. Vous pouvez le vérifier ici: Noms de balises dynamiques avec JAXB .

En bref:

  1. Une classe de conteneur pour @xmlAnyElement devrait être créé
  2. Un XmlAdapter peut être utilisé en paire avec @XmlJavaTypeAdapter pour convertir la classe de conteneur en mappage <>;
2
mec_test_1

Lorsque vous utilisez xml-apis-1.0, vous pouvez sérialiser et désérialiser ceci:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <map>
        <key>value</key>
        <key2>value2</key2>
    </map>
</root>

En utilisant ce code:

import Java.io.File;
import Java.util.HashMap;
import Java.util.List;
import Java.util.Map;
import Java.util.Map.Entry;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

@XmlRootElement
class Root {

    public XmlRawData map;

}

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Root root = (Root) unmarshaller.unmarshal(new File("src/input.xml"));

        System.out.println(root.map.getAsMap());

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }
}

class XmlRawData {

    @XmlAnyElement
    public List<Element> elements;

    public void setFromMap(Map<String, String> values) {

        Document document;
        try {
            document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }

        for (Entry<String, String> entry : values.entrySet()) {
            Element mapElement = document.createElement(entry.getKey());
            mapElement.appendChild(document.createTextNode(entry.getValue()));
            elements.add(mapElement);
        }
    }

    public Map<String, String> getAsMap() {
        Map<String, String> map = new HashMap<String, String>();

        for (Element element : elements) {
            if (element.getNodeType() == Node.ELEMENT_NODE) {
                map.put(element.getLocalName(), element.getFirstChild().getNodeValue());
            }
        }

        return map;
    }
}
0
slartidan