¿La mejor forma de codificar datos de texto para XML en Java?
encoding (18)
¡Esta pregunta tiene ocho años y todavía no es una respuesta completamente correcta! No, no debería tener que importar una API de terceros para realizar esta sencilla tarea. Mal consejo.
El siguiente método:
- manejar correctamente los caracteres fuera del plano multilingüe básico
- caracteres de escape necesarios en XML
- escape cualquier carácter no ASCII, que es opcional pero común
- reemplace caracteres ilegales en XML 1.0 con el carácter de sustitución Unicode. Aquí no hay la mejor opción, eliminarlos es igual de válido.
Intenté optimizar para el caso más común, y al mismo tiempo me aseguré de que podías canalizar / dev / random a través de esto y obtener una cadena válida en 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("�"); // Unicode replacement character
} else {
switch(c) {
case ''&'': sb.append("&"); break;
case ''>'': sb.append(">"); break;
case ''<'': sb.append("<"); break;
// Uncomment next two if encoding for an XML attribute
// case ''/''' sb.append("'"); break;
// case ''/"'' sb.append("""); 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("�"); // Unicode replacement character
} else {
sb.append("&#x");
sb.append(Integer.toHexString(c));
sb.append('';'');
}
}
return sb.toString();
}
Editar: para aquellos que siguen insistiendo en que es absurdo escribir su propio código para esto cuando hay API de Java perfectamente buenas para tratar con XML, es posible que desee saber que la API StAX incluida con Oracle Java 8 (no he probado otras ) no codifica el contenido de CDATA correctamente: no escapa]]> secuencias en el contenido. Una biblioteca de terceros, incluso una que sea parte del núcleo de Java, no siempre es la mejor opción.
Muy similar a esta pregunta , a excepción de Java.
¿Cuál es la forma recomendada de codificación de cadenas para una salida XML en Java? Las cadenas pueden contener caracteres como "&", "<", etc.
Aquí hay una solución fácil y ¡también es genial para codificar caracteres acentuados!
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);
Salidas
Hi Lârry & Môe!
El comportamiento de StringEscapeUtils.escapeXml () ha cambiado de Commons Lang 2.5 a 3.0. Ahora ya no escapa caracteres Unicode mayores que 0x7f.
Esto es algo bueno, el viejo método era estar un poco ansioso por escapar de entidades que podrían simplemente insertarse en un documento utf8.
Los nuevos escapes que se incluirán en Google Guava 11.0 también parecen prometedores: http://code.google.com/p/guava-libraries/issues/detail?id=799
Esto me ha funcionado bien para proporcionar una versión escapada de una cadena de texto:
public class XMLHelper {
/**
* Returns the string where all non-ascii and <, &, > are encoded as numeric entities. I.e. "<A & B >"
* .... (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();
}
}
Intente codificar el XML usando el 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());
Muy simple: use una biblioteca XML. De esa manera, en realidad será correcto en lugar de requerir un conocimiento detallado de los bits de la especificación XML.
Nota: su pregunta es acerca de escapar , no de codificar . Escapar es usar <, etc. para permitir que el analizador distinga entre "este es un comando XML" y "esto es algo de texto". La codificación es lo que especifica en el encabezado XML (UTF-8, ISO-8859-1, etc.).
En primer lugar, como todos los demás dijeron, use una biblioteca XML. XML parece simple, pero la codificación + escapatoria es vudú oscuro (que notará tan pronto como encuentre diéresis umlauts y cosas japonesas y otras cosas raras como " dígitos de ancho completo " (& # FF11; es 1)). Mantener XML legible para los humanos es una tarea de Sísifo.
Sugiero que nunca intentes ser inteligente sobre la codificación de texto y el escape en XML. Pero no dejes que eso te impida intentarlo; solo recuerda cuando te muerde (y lo hará).
Dicho esto, si usa solo UTF-8, para hacer las cosas más legibles, puede considerar esta estrategia:
- Si el texto contiene ''<'', ''>'' o ''&'', envuélvalo en
<![CDATA[ ... ]]>
- Si el texto no contiene estos tres caracteres, no lo desvíe.
Estoy usando esto en un editor de SQL y permite a los desarrolladores cortar y pegar SQL de una herramienta SQL de terceros en el XML sin preocuparse por el escape. Esto funciona porque el SQL no puede contener diéresis en nuestro caso, así que estoy a salvo.
Para aquellos que buscan la solución más rápida de escribir: use métodos de commons-lang :
-
StringEscapeUtils.escapeXml10()
para xml 1.0 -
StringEscapeUtils.escapeXml11()
para xml 1.1 - commons.apache.org/proper/commons-lang/javadocs/api-3.3/org/… ahora está en desuso, pero se usaba comúnmente en el pasado
Recuerde incluir la dependencia:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version> <!--check current version! -->
</dependency>
Para escapar de los caracteres XML, la forma más fácil es utilizar el proyecto Apache Commons Lang, JAR descargable desde: http://commons.apache.org/lang/
La clase es esta: org.apache.commons.lang3.StringEscapeUtils;
Tiene un método llamado "escapeXml", que devolverá un String apropiadamente escapado.
Prueba esto:
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("<"); break;
case ''>'': sb.append(">"); break;
case ''/"'': sb.append("""); break;
case ''&'': sb.append("&"); break;
case ''/''': sb.append("'"); break;
default:
if(c>0x7e) {
sb.append("&#"+((int)c)+";");
}else
sb.append(c);
}
}
return sb.toString();
}
Puede usar la biblioteca Enterprise Security API (ESAPI) , que proporciona métodos como encodeForXML
y encodeForXMLAttribute
. Eche un vistazo a la documentación de la interfaz del Encoder ; también contiene ejemplos de cómo crear una instancia de DefaultEncoder .
Si bien el idealismo dice que use una biblioteca XML, en mi humilde opinión, si tiene una idea básica de XML, entonces el sentido común y el rendimiento lo definen todo. Es posiblemente más legible también. Aunque usar las rutinas de escape de una biblioteca es probablemente una buena idea.
Considere esto: XML estaba destinado a ser escrito por humanos.
Use bibliotecas para generar XML cuando tener su XML como un "objeto" mejor modela su problema. Por ejemplo, si los módulos conectables participan en el proceso de construcción de este XML.
Editar: en cuanto a cómo escapar de XML en las plantillas, el uso de CDATA o escapeXml(string)
de JSTL son dos buenas soluciones, escapeXml(string)
se puede usar así:
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<item>${fn:escapeXml(value)}</item>
Si bien estoy de acuerdo con Jon Skeet en principio, a veces no tengo la opción de usar una biblioteca XML externa. Y me parece peculiar que las dos funciones para escapar / unescape de un valor simple (atributo o etiqueta, no el documento completo) no estén disponibles en las bibliotecas XML estándar incluidas con Java.
Como resultado y en función de las diferentes respuestas que he visto publicadas aquí y en todas partes, aquí está la solución que he terminado creando (nada funcionaba como una simple copia / pega):
public final static String ESCAPE_CHARS = "<>&/"/'";
public final static List<String> ESCAPE_STRINGS = Collections.unmodifiableList(Arrays.asList(new String[] {
"<"
, ">"
, "&"
, """
, "'"
}));
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;
}
Lo anterior acomoda varias cosas diferentes:
- evita usar la lógica basada en caracteres hasta que sea absolutamente necesario - mejora la compatibilidad de Unicode
- intenta ser lo más eficiente posible dado que la probabilidad es que la segunda condición "si" es probablemente la vía más utilizada
- es una función pura; es decir, es seguro para subprocesos
- optimiza muy bien con el recolector de basura devolviendo el contenido de StringBuilder solo si algo realmente cambió; de lo contrario, se devuelve la cadena original
En algún momento, escribiré la inversión de esta función en Unescaped (). Simplemente no tengo tiempo para hacer eso hoy. Cuando lo haga, vendré a actualizar esta respuesta con el código. :)
Solo usa.
<![CDATA[ your text here ]]>
Esto permitirá cualquier caracter excepto el final
]]>
Para que pueda incluir caracteres que serían ilegales, como & y>. Por ejemplo.
<element><![CDATA[ characters such as & and > are allowed ]]></element>
Sin embargo, los atributos deberán escaparse ya que los bloques CDATA no se pueden usar para ellos.
Use JAXP y olvídese del manejo de texto que se hará automáticamente.
StringEscapeUtils.escapeXml()
no escapa a los caracteres de control (<0x20). XML 1.1 permite caracteres de control; XML 1.0 no. Por ejemplo, XStream.toXML()
felizmente serializará los caracteres de control de un objeto Java en XML, que un analizador XML 1.0 rechazará.
Para escapar de los personajes de control con Apache commons-lang, use
NumericEntityEscaper.below(0x20).translate(StringEscapeUtils.escapeXml(str))
Como otros han mencionado, usar una biblioteca XML es la manera más fácil. Si quieres escapar de ti mismo, puedes mirar StringEscapeUtils
desde la biblioteca de Apache Commons Lang .
public String escapeXml(String s) {
return s.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("/"", """).replaceAll("''", "'");
}