2011-03-28 18 views
6

He pasado el último día intentando extraer un nodo XML del siguiente documento y no puedo comprender los matices de los espacios de nombres XML para que funcione.XPath, XML Namespaces y Java

El archivo XML es grande para colocar en total, de modo que aquí es la parte que me preocupa:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> 
<XFDL xmlns="http://www.PureEdge.com/XFDL/6.5" xmlns:custom="http://www.PureEdge.com/XFDL/Custom" xmlns:designer="http://www.PureEdge.com/Designer/6.1" xmlns:pecs="http://www.PureEdge.com/PECustomerService" xmlns:xfdl="http://www.PureEdge.com/XFDL/6.5"> 
    <globalpage sid="global"> 
     <global sid="global"> 
     <xmlmodel xmlns:xforms="http://www.w3.org/2003/xforms"> 
      <instances> 
       <xforms:instance id="metadata"> 
        <form_metadata> 
        <metadataver version="1.0"/> 
        <metadataverdate> 
         <date day="05" month="Jul" year="2005"/> 
        </metadataverdate> 
        <title> 
         <documentnbr number="2062" prefix.army="DA" scope="army" suffix=""/> 
         <longtitle>HAND RECEIPT/ANNEX NUMBER </longtitle> 
        </title> 

El documento continúa y está bien formado hasta el fondo. Estoy intentando extraer el atributo "número" de la etiqueta "documentnbr" (tres desde abajo).

El código que estoy usando para hacer esto es el siguiente:

/*** 
    * Locates the Document Number information in the file and returns the form number. 
    * @return File's self-declared number. 
    * @throws InvalidFormException Thrown when XPath cannot find the "documentnbr" element in the file. 
    */ 
    public String getFormNumber() throws InvalidFormException 
    { 
     try{ 
      XPath xPath = XPathFactory.newInstance().newXPath(); 
      xPath.setNamespaceContext(new XFDLNamespaceContext()); 

      Node result = (Node)xPath.evaluate(QUERY_FORM_NUMBER, doc, XPathConstants.NODE); 
      if(result != null) { 
       return result.getNodeValue(); 
      } else { 
       throw new InvalidFormException("Unable to identify form."); 
      } 

     } catch (XPathExpressionException err) { 
      throw new InvalidFormException("Unable to find form number in file."); 
     } 

    } 

Dónde QUERY_FORM_NUMBER es mi expresión XPath, y XFDLNamespaceContext implementa NamespaceContext y se ve así:

public class XFDLNamespaceContext implements NamespaceContext { 

    @Override 
    public String getNamespaceURI(String prefix) { 
     if (prefix == null) throw new NullPointerException("Invalid Namespace Prefix"); 
     else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) 
      return "http://www.PureEdge.com/XFDL/6.5"; 
     else if ("custom".equals(prefix)) 
      return "http://www.PureEdge.com/XFDL/Custom"; 
     else if ("designer".equals(prefix)) 
      return "http://www.PureEdge.com/Designer/6.1"; 
     else if ("pecs".equals(prefix)) 
      return "http://www.PureEdge.com/PECustomerService"; 
     else if ("xfdl".equals(prefix)) 
      return "http://www.PureEdge.com/XFDL/6.5";  
     else if ("xforms".equals(prefix)) 
      return "http://www.w3.org/2003/xforms"; 
     else  
      return XMLConstants.NULL_NS_URI; 
    } 

    @Override 
    public String getPrefix(String arg0) { 
     // TODO Auto-generated method stub 
     return null; 
    } 

    @Override 
    public Iterator getPrefixes(String arg0) { 
     // TODO Auto-generated method stub 
     return null; 
    } 

} 

I' he intentado muchas consultas XPath diferentes, pero sigo sintiendo que esto debería funcionar:

protected static final String QUERY_FORM_NUMBER = 
     "/globalpage/global/xmlmodel/xforms:instances/instance" + 
     "/form_metadata/title/documentnbr[number]"; 

Desafortunadamente, no funciona y recibo continuamente un retorno nulo.

He hecho una buena cantidad de lectura here, here y here, pero nada ha sido suficientemente esclarecedor para ayudarme a conseguir este funcionamiento.

Estoy casi seguro de que me voy a enfrentar a la palma de la mano cuando descubro esto, pero estoy realmente al borde de la muerte.

Gracias por leer todo esto y gracias de antemano por la ayuda.

-Andy

Respuesta

5

Aha, me trató de depurar su expresión + tengo que trabajar. Te perdiste algunas cosas. Esta expresión XPath debe hacerlo:

/XFDL/globalpage/global/xmlmodel/instances/instance/form_metadata/title/documentnbr/@number 
  1. es necesario incluir el elemento raíz (XFDL en este caso)
  2. no me acaban de tener que utilizar cualquier espacio de nombres en la expresión por alguna razón. No estoy seguro por qué. Si este es el caso, entonces nunca se llama a NamespaceContext.getNamespaceURI(). Si reemplazo instance con xforms:instance, se llamará getNamespaceURI() una vez con xforms como argumento de entrada, pero el programa arroja una excepción.
  3. La sintaxis de los valores de los atributos es @attr, no [attr].

Mi completa código de ejemplo:

import java.io.File; 
import java.io.IOException; 
import java.util.Collections; 
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.Map; 

import javax.xml.XMLConstants; 
import javax.xml.namespace.NamespaceContext; 
import javax.xml.parsers.DocumentBuilder; 
import javax.xml.parsers.DocumentBuilderFactory; 
import javax.xml.parsers.ParserConfigurationException; 
import javax.xml.xpath.XPath; 
import javax.xml.xpath.XPathConstants; 
import javax.xml.xpath.XPathExpressionException; 
import javax.xml.xpath.XPathFactory; 

import org.w3c.dom.Document; 
import org.w3c.dom.Node; 
import org.xml.sax.SAXException; 

public class XPathNamespaceExample { 
    static public class MyNamespaceContext implements NamespaceContext { 
     final private Map<String, String> prefixMap; 
     MyNamespaceContext(Map<String, String> prefixMap) 
     { 
      if (prefixMap != null) 
      { 
       this.prefixMap = Collections.unmodifiableMap(new HashMap<String, String>(prefixMap)); 
      } 
      else 
      { 
       this.prefixMap = Collections.emptyMap(); 
      } 
     } 
     public String getPrefix(String namespaceURI) { 
      // TODO Auto-generated method stub 
      return null; 
     } 
     public Iterator getPrefixes(String namespaceURI) { 
      // TODO Auto-generated method stub 
      return null; 
     } 
     public String getNamespaceURI(String prefix) { 
       if (prefix == null) throw new NullPointerException("Invalid Namespace Prefix"); 
       else if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) 
        return "http://www.PureEdge.com/XFDL/6.5"; 
       else if ("custom".equals(prefix)) 
        return "http://www.PureEdge.com/XFDL/Custom"; 
       else if ("designer".equals(prefix)) 
        return "http://www.PureEdge.com/Designer/6.1"; 
       else if ("pecs".equals(prefix)) 
        return "http://www.PureEdge.com/PECustomerService"; 
       else if ("xfdl".equals(prefix)) 
        return "http://www.PureEdge.com/XFDL/6.5";  
       else if ("xforms".equals(prefix)) 
        return "http://www.w3.org/2003/xforms"; 
       else  
        return XMLConstants.NULL_NS_URI; 
     } 


    } 

    protected static final String QUERY_FORM_NUMBER = 
     "/XFDL/globalpage/global/xmlmodel/xforms:instances/instance" + 
     "/form_metadata/title/documentnbr[number]"; 

    public static void main(String[] args) { 
     try 
     { 
      DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); 
      DocumentBuilder docBuilder = dbfac.newDocumentBuilder(); 
      Document doc = docBuilder.parse(new File(args[0])); 
      System.out.println(extractNodeValue(doc, "/XFDL/globalpage/@sid")); 
      System.out.println(extractNodeValue(doc, "/XFDL/globalpage/global/xmlmodel/instances/instance/@id")); 
      System.out.println(extractNodeValue(doc, "/XFDL/globalpage/global/xmlmodel/instances/instance/form_metadata/title/documentnbr/@number")); 
     } catch (SAXException e) { 
      e.printStackTrace(); 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } catch (ParserConfigurationException e) { 
      e.printStackTrace(); 
     } 
    } 

    private static String extractNodeValue(Document doc, String expression) { 
     try{ 

      XPath xPath = XPathFactory.newInstance().newXPath(); 
      xPath.setNamespaceContext(new MyNamespaceContext(null)); 

      Node result = (Node)xPath.evaluate(expression, doc, XPathConstants.NODE); 
      if(result != null) { 
       return result.getNodeValue(); 
      } else { 
       throw new RuntimeException("can't find expression"); 
      } 

     } catch (XPathExpressionException err) { 
      throw new RuntimeException(err); 
     } 
    } 
} 
+0

La solución perfecta, todavía no entiendo muy bien los espacios de nombres, pero al al menos el código funciona ahora. Muchas gracias. – MrWizard54

+8

@Jason: "No terminé necesitando usar espacios de nombres en la expresión por algún motivo". En la implementación estándar de Java, DocumentBuilderFactory produce namespace-_unaware_ parsers de forma predeterminada. Agregar 'dbfac.SetNamespaceAware (true)' antes de generar DocumentBuilder puede cambiar los resultados. –

3

SAX (alternativa a XPath) Versión:

SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); 
final String[] number = new String[1]; 
DefaultHandler handler = new DefaultHandler() 
{   
    @Override 
    public void startElement(String uri, String localName, String qName, 
    Attributes attributes) throws SAXException 
    { 
     if (qName.equals("documentnbr")) 
      number[0] = attributes.getValue("number"); 
    } 
}; 
saxParser.parse("input.xml", handler); 
System.out.println(number[0]); 

veo que es más complicado de usar XPath con espacios de nombres, ya que debe ser (mi opinión).Aquí está mi (simple) Código:

XPath xpath = XPathFactory.newInstance().newXPath(); 

NamespaceContextMap contextMap = new NamespaceContextMap(); 
contextMap.put("custom", "http://www.PureEdge.com/XFDL/Custom"); 
contextMap.put("designer", "http://www.PureEdge.com/Designer/6.1"); 
contextMap.put("pecs", "http://www.PureEdge.com/PECustomerService"); 
contextMap.put("xfdl", "http://www.PureEdge.com/XFDL/6.5"); 
contextMap.put("xforms", "http://www.w3.org/2003/xforms"); 
contextMap.put("", "http://www.PureEdge.com/XFDL/6.5"); 

xpath.setNamespaceContext(contextMap); 
String expression = "//:documentnbr/@number"; 
InputSource inputSource = new InputSource("input.xml"); 
String number; 
number = (String) xpath.evaluate(expression, inputSource, XPathConstants.STRING); 
System.out.println(number); 

Usted puede obtener la clase NamespaceContextMap (no la mía) de here (licencia GPL). También hay error 6376058.

+0

Si no estuviera usando DOM/XPath en ningún otro lado de la aplicación, seguiría esta ruta, pero actualmente me he dirigido en otra dirección. ¿Existe una sabiduría convencional sobre la mezcla dentro de una aplicación? – MrWizard54

+0

Puede usar DOM/XPath con el segundo código (xpath.evalute también toma el objeto de documento). En mi opinión, es mejor usar la clase NamespaceContextMap (debería estar en JDK). –

2

Eche un vistazo a la biblioteca XPathAPI. Es una forma más simple de usar XPath sin jugar con la API Java de bajo nivel, especialmente cuando se trata de espacios de nombres.

el código para obtener el atributo number sería:

String num = XPathAPI.selectSingleNodeAsString(doc, '//documentnbr/@number'); 

espacios de nombres se extraen automáticamente del nodo raíz (doc en este caso). En caso de tener que definir explícitamente los espacios de nombres adicionales que puede utilizar esto:

Map<String, String> nsMap = new HashMap<String, String>(); 
nsMap.put("xforms", "http://www.w3.org/2003/xforms"); 

String num = 
    XPathAPI.selectSingleNodeAsString(doc, '//documentnbr/@number', nsMap); 

(Negación:. Soy el autor de la biblioteca)