Melhor maneira de codificar dados de texto para XML em Java?

93

Muito semelhante a esta questão , exceto para Java.

Qual é a maneira recomendada de codificar strings para uma saída XML em Java. As strings podem conter caracteres como "&", "<", etc.

Epaga
fonte

Respostas:

40

Muito simplesmente: use uma biblioteca XML. Dessa forma, será realmente correto em vez de exigir conhecimento detalhado de bits da especificação XML.

Jon Skeet
fonte
25
Você pode recomendar essa biblioteca? (Acho surpreendente que esta não seja uma parte padrão do Java edição 5 ... uma tarefa tão comum).
Tim Cooper
4
XML é parte da estrutura Java padrão - procure em org.w3c.sax e org.w3c.dom. No entanto, também existem algumas estruturas mais fáceis de usar, como JDom. Observe que pode não haver um método de "codificação de strings para saída XML" - eu estava mais recomendando que toda a tarefa XML fosse feita com uma biblioteca em vez de apenas fazer bits por vez com a manipulação de strings.
Jon Skeet
1
Este não é um conselho tão útil ao produzir XHTML - FlyingSaucer requer XML, mas não há nenhuma maneira de criar um modelo através de uma biblioteca XML :). Felizmente, StringTemplate me permite escapar rapidamente de todos os objetos String.
Stephen
4
@mice: A questão é Java marcada, e Java tem muitas bibliotecas XML. Na verdade, existem APIs XML incorporadas ao Java, então não há necessidade de adicionar mais nada ... mas mesmo que você o fizesse, algumas centenas de K raramente são um problema fora do celular atualmente. Mesmo se não fosse Java, eu seria muito cauteloso ao desenvolver em uma plataforma que não tivesse APIs XML ...
Jon Skeet
2
@ ratos: A API DOM é perfeitamente capaz de gerar XML. Ou existem bibliotecas de terceiros bastante pequenas. (O arquivo jar do JDom tem 114K, por exemplo.) Usar uma API XML ainda é a maneira recomendada de criar XML.
Jon Skeet,
123

Como outros mencionaram, usar uma biblioteca XML é a maneira mais fácil. Se você quiser escapar-se, você pode olhar para StringEscapeUtilsa partir do Apache Commons Lang biblioteca.

Fabian Steeg
fonte
Este pode ser o caminho a percorrer se você não se preocupar com a exatidão absoluta, por exemplo, se estiver montando um protótipo.
Chase Seibert
2
Use StringEscapeUtils.escapeXml(str)de commons-lang. Eu o uso no aplicativo App Engine - funciona perfeitamente. Aqui está o Java Doc para esta função:
Oleg K
O método escapeXml de StringEscapeUtils parece ser um pouco caro. Existe um método mais eficiente que opera em um StringBuffer em vez de uma String?
CKing
Este método funciona para conteúdo e atributos XML? Para mim, parece que não funciona para atributos. Não parece escapar \t, \ne \r.
Lii
@Lii e \t, \nou \rprecisa ser escapado?
Betlista
20

Apenas use.

<![CDATA[ your text here ]]>

Isso permitirá qualquer caractere, exceto o final

]]>

Portanto, você pode incluir caracteres que seriam ilegais, como & e>. Por exemplo.

<element><![CDATA[ characters such as & and > are allowed ]]></element>

No entanto, os atributos precisarão ser escapados, pois os blocos CDATA não podem ser usados ​​para eles.

ng.
fonte
11
Na maioria dos casos, não é isso que você deve fazer. Muitas pessoas abusam das tags CDATA. A intenção do CDATA é dizer ao processador para não processá-lo como XML e apenas transmiti-lo. Se você está tentando criar um arquivo XML, deve criar XML, não apenas passar bytes por algum elemento de agrupamento.
Mads Hansen
2
@Mads, usar CDATA resulta em um arquivo XML válido, portanto, é tão bom quanto fazê-lo da "maneira certa". Se você não gostar, analise-o depois, a identidade o transforme e imprima.
Thorbjørn Ravn Andersen
24
Se você agrupar o texto em um elemento CDATA, terá que escapar o marcador de fechamento CDATA: "]]>" ... exceto que você não pode escapar disso. Portanto, em vez disso, você tem que quebrar seu código em partes, colocando metade dos dados em um elemento CDATA e a outra metade em um segundo: <! [CDATA [Esses dados contêm um marcador de fechamento CDATA: "]]]]> <! [CDATA [> "é por isso que teve que ser dividido.]]> ... No final, pode ser muito mais simples simplesmente escapar de '<', '>' e '&'. É claro que muitos aplicativos ignoram o problema potencial com os marcadores de fechamento CDATA nos dados. Ignorância é uma bênção, eu acho. :)
Stijn de Witt
3
@StijndeWitt está absolutamente correto. CDATA não é uma panacéia para caracteres especiais de escape.
dnault
Esta é uma má ideia. CDATA não permite nenhum caractere fora da codificação XML.
Florian F
14

Isso funcionou bem para mim fornecer uma versão com escape de uma string de texto:

public class XMLHelper {

/**
 * Returns the string where all non-ascii and <, &, > are encoded as numeric entities. I.e. "&lt;A &amp; B &gt;"
 * .... (insert result here). The result is safe to include anywhere in a text field in an XML-string. If there was
 * no characters to protect, the original string is returned.
 * 
 * @param originalUnprotectedString
 *            original string which may contain characters either reserved in XML or with different representation
 *            in different encodings (like 8859-1 and UFT-8)
 * @return
 */
public static String protectSpecialCharacters(String originalUnprotectedString) {
    if (originalUnprotectedString == null) {
        return null;
    }
    boolean anyCharactersProtected = false;

    StringBuffer stringBuffer = new StringBuffer();
    for (int i = 0; i < originalUnprotectedString.length(); i++) {
        char ch = originalUnprotectedString.charAt(i);

        boolean controlCharacter = ch < 32;
        boolean unicodeButNotAscii = ch > 126;
        boolean characterWithSpecialMeaningInXML = ch == '<' || ch == '&' || ch == '>';

        if (characterWithSpecialMeaningInXML || unicodeButNotAscii || controlCharacter) {
            stringBuffer.append("&#" + (int) ch + ";");
            anyCharactersProtected = true;
        } else {
            stringBuffer.append(ch);
        }
    }
    if (anyCharactersProtected == false) {
        return originalUnprotectedString;
    }

    return stringBuffer.toString();
}

}
Thorbjørn Ravn Andersen
fonte
1
stringBuffer.append ("& #" + (int) ch + ";"); Isso não funcionará para caracteres multibyte. Estou enfrentando isso agora com um personagem emoji, sequência UTF8 F0 9F 98 8D.
Kylar
14

Experimente isto:

String xmlEscapeText(String t) {
   StringBuilder sb = new StringBuilder();
   for(int i = 0; i < t.length(); i++){
      char c = t.charAt(i);
      switch(c){
      case '<': sb.append("&lt;"); break;
      case '>': sb.append("&gt;"); break;
      case '\"': sb.append("&quot;"); break;
      case '&': sb.append("&amp;"); break;
      case '\'': sb.append("&apos;"); break;
      default:
         if(c>0x7e) {
            sb.append("&#"+((int)c)+";");
         }else
            sb.append(c);
      }
   }
   return sb.toString();
}
Pointer Null
fonte
8
Você tem pelo menos dois bugs que eu posso ver. Um é sutil, o outro não. Eu não teria esse bug - porque eu não reinventaria a roda em primeiro lugar.
Jon Skeet,
1
E iterar por meio de strings Unicode é um pouco mais complicado. Veja aqui: stackoverflow.com/q/1527856/402322
ceving
1
Não tenho certeza se é sutil, mas seria melhor considerar o caso em que t==null.
Myobis
1
@ user1003916: O escape de XML é projetado para converter qualquer & ocorrência em & amp; então é assim que tem que funcionar. Se você já escapou da string, a culpa é sua.
Pointer Null
3
Estou feliz com a versão final. Java SE é compacto, rápido e eficiente. Fazer apenas o que precisa ser feito em vez de baixar outros 100 MB de bloatware é sempre melhor para mim.
Roger F. Gay de
11

Esta pergunta tem oito anos e ainda não é uma resposta totalmente correta! Não, você não deve ter que importar uma API de terceiros inteira para fazer esta tarefa simples. Mau conselho.

O seguinte método irá:

  • lidar corretamente com caracteres fora do plano multilíngue básico
  • caracteres de escape exigidos em XML
  • escapar de quaisquer caracteres não ASCII, o que é opcional, mas comum
  • substitua caracteres ilegais em XML 1.0 pelo caractere de substituição Unicode. Não há melhor opção aqui - removê-los é igualmente válido.

Tentei otimizar para o caso mais comum, enquanto ainda garantindo que você poderia canalizar / dev / random por meio disso e obter uma string válida em XML.

public static String encodeXML(CharSequence s) {
    StringBuilder sb = new StringBuilder();
    int len = s.length();
    for (int i=0;i<len;i++) {
        int c = s.charAt(i);
        if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) {
            c = ((c-0xd7c0)<<10) | (s.charAt(++i)&0x3ff);    // UTF16 decode
        }
        if (c < 0x80) {      // ASCII range: test most common case first
            if (c < 0x20 && (c != '\t' && c != '\r' && c != '\n')) {
                // Illegal XML character, even encoded. Skip or substitute
                sb.append("&#xfffd;");   // Unicode replacement character
            } else {
                switch(c) {
                  case '&':  sb.append("&amp;"); break;
                  case '>':  sb.append("&gt;"); break;
                  case '<':  sb.append("&lt;"); break;
                  // Uncomment next two if encoding for an XML attribute
//                  case '\''  sb.append("&apos;"); break;
//                  case '\"'  sb.append("&quot;"); break;
                  // Uncomment next three if you prefer, but not required
//                  case '\n'  sb.append("&#10;"); break;
//                  case '\r'  sb.append("&#13;"); break;
//                  case '\t'  sb.append("&#9;"); break;

                  default:   sb.append((char)c);
                }
            }
        } else if ((c >= 0xd800 && c <= 0xdfff) || c == 0xfffe || c == 0xffff) {
            // Illegal XML character, even encoded. Skip or substitute
            sb.append("&#xfffd;");   // Unicode replacement character
        } else {
            sb.append("&#x");
            sb.append(Integer.toHexString(c));
            sb.append(';');
        }
    }
    return sb.toString();
}

Edit: para aqueles que continuam a insistir que é tolice escrever seu próprio código para isso quando há APIs Java perfeitamente boas para lidar com XML, você pode gostar de saber que a API StAX incluída no Oracle Java 8 (eu não testei outras ) falha ao codificar o conteúdo CDATA corretamente: não escapa]]> sequências no conteúdo. Uma biblioteca de terceiros, mesmo que seja parte do núcleo do Java, nem sempre é a melhor opção.

Mike B
fonte
+1 para código autônomo. Apenas comparando seu código com a implementação de goiaba , estou me perguntando e quanto a '\ t', '\ n', '\ r'? Veja também notas em documentos de goiaba
jschnasse
2
Não há necessidade de escapar de \ n, \ r e \ t, eles são válidos, embora tornem a formatação um pouco feia. Modifiquei o código para mostrar como evitá-los, se for isso que você deseja.
Mike B
1
Não como "escapar]]>" no CDATA.
kmkaplan
1
Em seguida, ele deve rejeitar o conteúdo lançando uma IllegalArgumentException. Sob nenhuma circunstância ele deve alegar ter sido bem-sucedido, mas ainda assim gerar um XML inválido.
Mike B,
Em vez de substituir caracteres ilegais em XML 1.0 pelo caractere de substituição Unicode, você pode usar meus métodos aqui stackoverflow.com/a/59475093/3882565 .
stonar96
8

StringEscapeUtils.escapeXml()não escapa caracteres de controle (<0x20). XML 1.1 permite caracteres de controle; XML 1.0 não. Por exemplo, XStream.toXML()serializará alegremente os caracteres de controle de um objeto Java em XML, que um analisador XML 1.0 rejeitará.

Para escapar de caracteres de controle com Apache commons-lang, use

NumericEntityEscaper.below(0x20).translate(StringEscapeUtils.escapeXml(str))
Steve Mitchell
fonte
7
public String escapeXml(String s) {
    return s.replaceAll("&", "&amp;").replaceAll(">", "&gt;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
}
iCrazybest
fonte
5
Encadear replaceAllchamadas é muito ineficiente, especialmente para strings grandes. Cada chamada resulta na criação de um novo objeto String, que ficará parado até que o lixo seja coletado. Além disso, cada chamada requer um loop pela string novamente. Isso poderia ser consolidado em um único loop manual com comparações contra cada caractere de destino em cada iteração.
daiscog de
Essa deve ser a resposta aceita, mesmo que seja ineficiente. Ele resolve o problema em uma única linha.
Stimpson Cat
E tem muitos bugs. Veja este comentário acima
David Balažic
Para corrigir esses bugs, você também pode usar meu método aqui stackoverflow.com/a/59475093/3882565 . Observe que este não é um substituto, mas pode ser usado adicionalmente.
stonar96
6

Enquanto o idealismo diz para usar uma biblioteca XML, IMHO, se você tem uma ideia básica de XML, o bom senso e o desempenho dizem que é um modelo completo. É indiscutivelmente mais legível também. Embora usar as rotinas de escape de uma biblioteca seja provavelmente uma boa ideia.

Considere o seguinte: XML foi feito para ser escrito por humanos.

Use bibliotecas para gerar XML quando tiver seu XML como um "objeto" modelar melhor seu problema. Por exemplo, se os módulos conectáveis ​​participarem do processo de construção deste XML.

Editar: quanto a como realmente escapar XML em modelos, o uso de CDATA ou escapeXml(string)de JSTL são duas boas soluções, escapeXml(string)podem ser usadas assim:

<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

<item>${fn:escapeXml(value)}</item>
Amr Mostafa
fonte
6

O comportamento de StringEscapeUtils.escapeXml () foi alterado de Commons Lang 2.5 para 3.0. Agora ele não escapa mais caracteres Unicode maiores que 0x7f.

Isso é uma coisa boa, o método antigo era estar um pouco ansioso para escapar de entidades que poderiam apenas ser inseridas em um documento utf8.

Os novos escapers a serem incluídos no Google Guava 11.0 também parecem promissores: http://code.google.com/p/guava-libraries/issues/detail?id=799

Jasper Krijgsman
fonte
1
Este é o escaper XML do Guava: code.google.com/p/guava-libraries/source/browse/guava/src/com/… . Em geral, descobri que o Guava é melhor arquitetado do que o Apache Commons.
jhclark
5

Observação: sua pergunta é sobre como escapar , não sobre a codificação . Escaping está usando <, etc. para permitir que o analisador distinga entre "este é um comando XML" e "este é um texto". Codificação é o que você especifica no cabeçalho XML (UTF-8, ISO-8859-1, etc).

Em primeiro lugar, como todo mundo disse, use uma biblioteca XML. XML parece simples, mas o material de codificação + escape é vodu escuro (que você notará assim que encontrar tremas e japonês e outras coisas estranhas como " dígitos de largura total " (& # FF11; é 1)). Manter o XML legível por humanos é uma tarefa de Sísifo.

Eu sugiro que nunca tente ser inteligente sobre codificação de texto e escape em XML. Mas não deixe que isso o impeça de tentar; apenas lembre-se de quando ele te morde (e vai).

Dito isso, se você usar apenas UTF-8, para tornar as coisas mais legíveis, você pode considerar esta estratégia:

  • Se o texto contiver '<', '>' ou '&', envolva-o <![CDATA[ ... ]]>
  • Se o texto não contiver esses três caracteres, não o deforme.

Estou usando isso em um editor de SQL e permite que os desenvolvedores recortem e colem SQL de uma ferramenta SQL de terceiros no XML sem se preocupar em escapar. Isso funciona porque o SQL não pode conter tremas no nosso caso, então estou seguro.

Aaron Digulla
fonte
5

Embora concorde com Jon Skeet em princípio, às vezes não tenho a opção de usar uma biblioteca XML externa. E acho peculiar que as duas funções para escapar / unescape um valor simples (atributo ou tag, não documento completo) não estão disponíveis nas bibliotecas XML padrão incluídas com Java.

Como resultado e com base nas diferentes respostas que vi postadas aqui e em outros lugares, aqui está a solução que acabei criando (nada funcionou como um simples copiar / colar):

  public final static String ESCAPE_CHARS = "<>&\"\'";
  public final static List<String> ESCAPE_STRINGS = Collections.unmodifiableList(Arrays.asList(new String[] {
      "&lt;"
    , "&gt;"
    , "&amp;"
    , "&quot;"
    , "&apos;"
  }));

  private static String UNICODE_LOW =  "" + ((char)0x20); //space
  private static String UNICODE_HIGH = "" + ((char)0x7f);

  //should only use for the content of an attribute or tag      
  public static String toEscaped(String content) {
    String result = content;

    if ((content != null) && (content.length() > 0)) {
      boolean modified = false;
      StringBuilder stringBuilder = new StringBuilder(content.length());
      for (int i = 0, count = content.length(); i < count; ++i) {
        String character = content.substring(i, i + 1);
        int pos = ESCAPE_CHARS.indexOf(character);
        if (pos > -1) {
          stringBuilder.append(ESCAPE_STRINGS.get(pos));
          modified = true;
        }
        else {
          if (    (character.compareTo(UNICODE_LOW) > -1)
               && (character.compareTo(UNICODE_HIGH) < 1)
             ) {
            stringBuilder.append(character);
          }
          else {
            stringBuilder.append("&#" + ((int)character.charAt(0)) + ";");
            modified = true;
          }
        }
      }
      if (modified) {
        result = stringBuilder.toString();
      }
    }

    return result;
  }

O acima acomoda várias coisas diferentes:

  1. evita o uso de lógica baseada em char até que seja absolutamente necessário - melhora a compatibilidade com Unicode
  2. tenta ser o mais eficiente possível, dada a probabilidade de que a segunda condição "se" seja provavelmente o caminho mais usado
  3. é uma função pura; ou seja, é thread-safe
  4. otimiza muito bem com o coletor de lixo retornando apenas o conteúdo do StringBuilder se algo realmente mudou - caso contrário, a string original é retornada

Em algum momento, escreverei a inversão dessa função, toUnescaped (). Eu simplesmente não tenho tempo para fazer isso hoje. Quando eu fizer isso, irei atualizar esta resposta com o código. :)

caótico3quilíbrio
fonte
Parece muito bom para mim. Não desejo adicionar outro jar ao meu projeto para apenas um método. Se você conceder permissão, posso copiar e colar seu código no meu?
RuntimeException
1
@SatishMotwani Claro que você pode pegar o código acima e fazer com ele como quiser. É meu entendimento que qualquer código publicado no StackOverflow é considerado livre de direitos autorais (não é coberto como uma obra na totalidade). Por outro lado, seria extremamente difícil para alguém apresentar qualquer tipo de reivindicação de direitos autorais e esperar um tipo de resultado para si.
chaotic3quilibrium
1
Obrigado por permitir :-) Vou usá-lo.
RuntimeException
Você se esqueceu de lidar com personagens NUL. E talvez outras coisas também.
David Balažic
3

Para escapar de caracteres XML, a maneira mais fácil é usar o projeto Apache Commons Lang, JAR disponível para download em: http://commons.apache.org/lang/

A classe é esta: org.apache.commons.lang3.StringEscapeUtils;

Ele tem um método chamado "escapeXml", que retornará uma String com escape apropriado.

Greg Burdett
fonte
Atualização: escapeXml agora está obsoleto - use escapeXml10. Ref. Commons.apache.org/proper/commons-lang/javadocs/api-3.3/org/…
Daniel
3

Se você está procurando uma biblioteca para fazer o trabalho, tente:

  1. Guava 26.0 documentado aqui

    return XmlEscapers.xmlContentEscaper().escape(text);

    Nota: Também existe um xmlAttributeEscaper()

  2. Apache Commons Text 1.4 documentado aqui

    StringEscapeUtils.escapeXml11(text)

    Nota: Também existe um escapeXml10()método

jschnasse
fonte
1

Esta é uma solução fácil e ótima para codificar caracteres acentuados também!

String in = "Hi Lârry & Môe!";

StringBuilder out = new StringBuilder();
for(int i = 0; i < in.length(); i++) {
    char c = in.charAt(i);
    if(c < 31 || c > 126 || "<>\"'\\&".indexOf(c) >= 0) {
        out.append("&#" + (int) c + ";");
    } else {
        out.append(c);
    }
}

System.out.printf("%s%n", out);

Saídas

Hi L&#226;rry &#38; M&#244;e!
Mike
fonte
O "31" na primeira linha do "if" não deveria ser "32"; ou seja, menos do que o caractere de espaço? E se "31" deve permanecer, então não deveria ser corrigido para "if (c <= 31 || ..." (sinal de igual adicional após o sinal de menor que)?
caótico3quilibrium
1

Apenas substitua

 & with &amp;

E para outros personagens:

> with &gt;
< with &lt;
\" with &quot;
' with &apos;
Raman Rayat
fonte
0

Use JAXP e esqueça o tratamento de texto, isso será feito para você automaticamente.

Fernando Miguélez
fonte
Seu link está em espanhol, o que não é muito útil para a maioria de nós. Melhor é este .
Vivit
0

Tente codificar o XML usando o serializador Apache XML

//Serialize DOM
OutputFormat format    = new OutputFormat (doc); 
// as a String
StringWriter stringOut = new StringWriter ();    
XMLSerializer serial   = new XMLSerializer (stringOut, 
                                          format);
serial.serialize(doc);
// Display the XML
System.out.println(stringOut.toString());
K Victor Rajan
fonte
0

Aqui está o que descobri depois de pesquisar em todos os lugares em busca de uma solução:

Obtenha a biblioteca Jsoup:

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.12.1</version>
</dependency>

Então:

import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Entities
import org.jsoup.parser.Parser

String xml = '''<?xml version = "1.0"?>
<SOAP-ENV:Envelope
   xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
   SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">

   <SOAP-ENV:Body xmlns:m = "http://www.example.org/quotations">
      <m:GetQuotation>
         <m:QuotationsName> MiscroSoft@G>>gle.com </m:QuotationsName>
      </m:GetQuotation>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>'''



Document doc = Jsoup.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")), "UTF-8", "", Parser.xmlParser())
doc.outputSettings().charset("UTF-8")
doc.outputSettings().escapeMode(Entities.EscapeMode.base)

println doc.toString()

Espero que isso ajude alguém

Wizston
fonte