2010-08-27 9 views
8

J'ai une classe qui diffuse xml à partir d'une source tierce (je n'ai aucun contrôle sur le contenu). Voici l'extrait qui unmarshals:JAXB SAXParseException lors de la résolution d'un document avec un chemin d'accès relatif à la DTD

JAXBContext jContext = JAXBContext.newInstance("com.optimumlightpath.it.aspenoss.xsd"); 
Unmarshaller unmarshaller = jContext.createUnmarshaller() ; 
StringReader xmlStr = new StringReader(str.value); 
Connections conns = (Connections) unmarshaller.unmarshal(xmlStr); 

Connections est une classe générée DTD> xsd-> classe à l'aide xjc. Le paquet com.optimumlightpath.it.aspenoss.xsd contient toutes ces classes.

Le fichier xml I reçoit contient un chemin relatif dans DOCTYPE. En fait str.value ci-dessus contient:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> 
<!DOCTYPE Connections SYSTEM "./dtd/Connections.dtd"> 
<Connections> 
... 
</Connections> 

Cela va avec succès comme une application java 1.5. Afin d'éviter l'erreur ci-dessus, j'ai dû créer un répertoire ./dtd à partir de la racine du projet et inclure tous les fichiers dtd (je ne sais pas pourquoi je devais le faire mais nous y arriverons).

J'ai depuis créé un service web sur Tomcat5.5 qui utilise la classe ci-dessus. Je reçois [org.xml.sax.SAXParseException: Relative URI "./dtd/Connections.dtd"; can not be resolved without a document URI.] sur la ligne unmarshal. J'ai essayé de créer ./dtd dans chaque dossier relavant (racine du projet, WebContent, WEB-INF, répertoire de travail tomcat, etc) en vain. Question n ° 1: Où puis-je trouver ./dtd afin que la classe puisse le trouver lorsqu'il est exécuté en tant que service web Tomcat? Existe-t-il une configuration de tomcat ou de service que je dois faire pour que le répertoire soit reconnu? Question n ° 2: Pourquoi la classe a-t-elle besoin du fichier dtd en premier lieu? N'a-t-il pas toutes les informations dont il a besoin pour se marier dans les annotations de la classe dtd-> xsd->? J'ai lu de nombreux articles sur la désactivation de la validation, la définition de EntityResource et d'autres solutions, mais cette classe n'est pas toujours déployée en tant que service Web et je ne souhaite pas avoir deux trains de code.

Répondre

8

Lorsque unmarshalling d'un InputStream ou Reader l'analyseur ne connaît pas le systemId (uri/emplacement) du document, il ne peut pas résoudre des chemins relatifs. Il semble que l'analyseur tente de résoudre les références en utilisant le répertoire de travail en cours, qui ne fonctionne que lorsqu'il s'exécute à partir de l'ide ou de la ligne de commande. Afin de contourner ce comportement et de faire la résolution vous-même, vous devez mettre en œuvre un EntityResolver, comme Blaise Doughan mentionné. Après quelques expériences, j'ai trouvé un moyen standard de le faire. Vous devez démarshal d'un SAXSource, qui est à son tour construit à partir d'un XMLReader et un InputSource. Dans cet exemple, le dtd est situé à côté de la classe annotée et peut donc être trouvé dans le classpath.

Main.java

public class Main { 
    private static final String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces"; 
    private static final String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes"; 

    public static void main(String[] args) throws JAXBException, IOException, SAXException { 
     JAXBContext ctx = JAXBContext.newInstance(Root.class); 
     Unmarshaller unmarshaller = ctx.createUnmarshaller(); 

     XMLReader xmlreader = XMLReaderFactory.createXMLReader(); 
     xmlreader.setFeature(FEATURE_NAMESPACES, true); 
     xmlreader.setFeature(FEATURE_NAMESPACE_PREFIXES, true); 
     xmlreader.setEntityResolver(new EntityResolver() { 
      public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { 
       // TODO: Check if systemId really references root.dtd 
       return new InputSource(Root.class.getResourceAsStream("root.dtd")); 
      } 
     }); 

     String xml = "<!DOCTYPE root SYSTEM './root.dtd'><root><element>test</element></root>"; 
     InputSource input = new InputSource(new StringReader(xml)); 
     Source source = new SAXSource(xmlreader, input); 

     Root root = (Root)unmarshaller.unmarshal(source); 
     System.out.println(root.getElement()); 
    } 
} 

Root.java

@XmlRootElement 
@XmlAccessorType(XmlAccessType.FIELD) 
public class Root { 
    @XmlElement 
    private String element; 

    public String getElement() { 
     return element; 
    } 

    public void setElement(String element) { 
     this.element = element; 
    } 
} 

root.dtd

<?xml version="1.0" encoding="UTF-8"?> 
<!ELEMENT root (element)> 
<!ELEMENT element (#PCDATA)> 
+0

Jorn - ty pour votre réponse. J'essaie d'abord la suggestion de Blaise car elle nécessite le moins de changements de code. Mais vous avez tous les deux fourni des réponses très utiles. Y a-t-il un moyen de créditer les deux? –

+0

Mon approche fonctionnera, mais l'avantage de l'approche de Jorn est que vous restez agnostique JAXB qui est la solution la plus portable. –

2

Question n ° 2: Pourquoi la classe a-t-elle besoin du fichier dtd en premier lieu?

Ce n'est pas l'implémentation JAXB qui recherche la DTD, c'est l'analyseur sous-jacent.

Question n ° 1: Où puis-je trouver ./dtd afin que la classe peut trouver quand exécuter comme webservice tomcat?

Je ne suis pas sûr, mais au-dessous, je vais vous montrer une façon que vous pouvez faire ce travail en utilisant la mise en œuvre MOXy JAXB (je suis le chef de file de la technologie) qui fonctionne dans plusieurs environnements.

Solution proposée

Créer un EntityResolver qui charge la DTD du classpath. De cette façon, vous pouvez conditionner la DTD avec votre application et vous saurez toujours où elle se trouve quel que soit l'environnement de déploiement.

public class DtdEntityResolver implements EntityResolver { 

    public InputSource resolveEntity(String publicId, String systemId) 
      throws SAXException, IOException { 
     InputStream dtd = getClass().getClassLoader().getResourceAsStream("dtd/Connections.dtd"); 
     return new InputSource(dtd); 
    } 

} 

Ensuite, en utilisant la mise en œuvre Moxy JAXB vous pouvez renversera la mise en œuvre sous-jacente et définir le EntityResolver.

import org.eclipse.persistence.jaxb.JAXBHelper; 
... 
JAXBContext jContext = JAXBContext.newInstance("com.optimumlightpath.it.aspenoss.xsd"); 
Unmarshaller unmarshaller = jContext.createUnmarshaller() ; 
JAXBHelper.getUnmarshaller(unmarshaller).getXMLUnmarshaller().setEntityResolver(new DtdEntityResolver()); 
StringReader xmlStr = new StringReader(str.value); 
Connections conns =(Connections) unmarshaller.unmarshal(xmlStr); 
+0

Blaise - ty pour prendre le temps de répondre. Initialement, JAXBHelper s'est plaint que le unmarshaller n'était pas un unmarshaller d'eclipselink. J'ai donc remplacé javax.xml.bind.JAXBContext par org.eclipse.persistence.jaxb.JAXBContext et javax.xml.bind.Unmarshaller par org.eclipse.persistence.jaxb.JAXBUnmarshaller. Cependant, l'eclipselink JAXBContext renvoie un type JAVax JAXBContext. JAXBUnmarshaller nécessite un type eclipselink et je reçois des exceptions de cast si j'essaie de refondre. Des idées? –

+0

Vous devez ajouter 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 –

+0

Je peux essayer cela à un autre moment. J'ai été capable de faire fonctionner la réponse de Jorn bien que cela nécessitera plus de changements de code. Comme vous le dites, c'est le plus portable. Merci beaucoup à tous les deux !! Quand je reçois un représentant, je peux voter pour votre réponse. –

0

Voici une autre variation sur les réponses al prêt été donné en utilisant l'interface EntityResolver. Ma situation consistait à résoudre des entités XML externes relatives d'un fichier XML à un autre dans une hiérarchie de dossiers. Le paramètre au constructeur ci-dessous est le dossier "working" XML, pas le répertoire de travail du processus.

public class FileEntityResolver implements EntityResolver { 
    private static final URI USER_DIR = SystemUtils.getUserDir().toURI(); 

    private URI root; 

    public FileEntityResolver(File root) { 
     this.root = root.toURI(); 
    } 

    @Override @SuppressWarnings("resource") 
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { 
     URI systemURI; 
     try { 
      systemURI = new URI(systemId); 
     } catch (URISyntaxException e) { 
      return null; 
     } 

     URI relative = USER_DIR.relativize(systemURI); 
     URI resolved = root.resolve(relative); 

     File f = new File(resolved); 
     FileReader fr = new FileReader(f); 
     // SAX will close the file reader for us 
     return new InputSource(fr); 
    } 
}