¿Java equivalente al encodeURIComponent de JavaScript que produce una salida idéntica?

92

He estado experimentando con varios bits de código Java tratando de encontrar algo que codifique una cadena que contenga comillas, espacios y caracteres Unicode "exóticos" y produzca una salida idéntica a la función encodeURIComponent de JavaScript .

Mi cadena de prueba de tortura es: "A" B ± "

Si ingreso la siguiente declaración de JavaScript en Firebug:

encodeURIComponent('"A" B ± "');

—Entonces obtengo:

"%22A%22%20B%20%C2%B1%20%22"

Aquí está mi pequeño programa de prueba de Java:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class EncodingTest
{
  public static void main(String[] args) throws UnsupportedEncodingException
  {
    String s = "\"A\" B ± \"";
    System.out.println("URLEncoder.encode returns "
      + URLEncoder.encode(s, "UTF-8"));

    System.out.println("getBytes returns "
      + new String(s.getBytes("UTF-8"), "ISO-8859-1"));
  }
}

—Este programa genera:

URLEncoder.encode devuelve% 22A% 22 + B +% C2% B1 +% 22
getBytes devuelve "A" B ± "

¡Cerca, pero no puro! ¿Cuál es la mejor forma de codificar una cadena UTF-8 usando Java para que produzca la misma salida que la de JavaScript encodeURIComponent?

EDITAR: Estoy usando Java 1.4 para pasar a Java 5 en breve.

John Topley
fuente

Respuestas:

63

Al observar las diferencias de implementación, veo que:

MDC enencodeURIComponent() :

  • caracteres literales (representación de expresiones regulares): [-a-zA-Z0-9._*~'()!]

Documentación de Java 1.5.0 sobreURLEncoder :

  • caracteres literales (representación de expresiones regulares): [-a-zA-Z0-9._*]
  • el carácter de espacio " "se convierte en un signo más "+".

Básicamente, para obtener el resultado deseado, use URLEncoder.encode(s, "UTF-8")y luego realice un posprocesamiento:

  • reemplazar todas las apariciones de "+"con"%20"
  • Reemplazar todas las ocurrencias de "%xx"representar cualquiera de [~'()!]sus contrapartes literales
Tomalak
fuente
Ojalá hubieras escrito "Reemplaza todas las apariciones de"% xx "que representen cualquiera de [~ '()!] Por sus contrapartes literales" en un lenguaje simple. :( mi pequeña cabeza no es capaz de entenderlo .......
Shailendra Singh Rajawat
1
@Shailendra [~'()!]significa "~"o "'"o "("o ")"o "!". :) Sin embargo, recomiendo aprender los conceptos básicos de las expresiones regulares. (Tampoco amplié eso ya que al menos otras dos respuestas muestran el código Java respectivo).
Tomalak
3
Reemplazar todas las apariciones de "+"con "%20"es potencialmente destructivo, al igual "+"que un carácter legal en las rutas de URI (aunque no en la cadena de consulta). Por ejemplo, "a + b c" debe codificarse como "a+b%20c"; esta solución lo convertiría en "a%20b%20c". En su lugar, utilice new URI(null, null, value, null).getRawPath().
Chris Nitchie
@ChrisNitchie Ese no era el punto de la pregunta. La pregunta era "¿Java equivalente al encodeURIComponent de JavaScript que produce una salida idéntica?" , no "¿Función genérica del componente codificador URI de Java?" .
Tomalak
118

Esta es la clase que se me ocurrió al final:

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * Utility class for JavaScript compatible UTF-8 encoding and decoding.
 * 
 * @see http://stackoverflow.com/questions/607176/java-equivalent-to-javascripts-encodeuricomponent-that-produces-identical-output
 * @author John Topley 
 */
public class EncodingUtil
{
  /**
   * Decodes the passed UTF-8 String using an algorithm that's compatible with
   * JavaScript's <code>decodeURIComponent</code> function. Returns
   * <code>null</code> if the String is <code>null</code>.
   *
   * @param s The UTF-8 encoded String to be decoded
   * @return the decoded String
   */
  public static String decodeURIComponent(String s)
  {
    if (s == null)
    {
      return null;
    }

    String result = null;

    try
    {
      result = URLDecoder.decode(s, "UTF-8");
    }

    // This exception should never occur.
    catch (UnsupportedEncodingException e)
    {
      result = s;  
    }

    return result;
  }

  /**
   * Encodes the passed String as UTF-8 using an algorithm that's compatible
   * with JavaScript's <code>encodeURIComponent</code> function. Returns
   * <code>null</code> if the String is <code>null</code>.
   * 
   * @param s The String to be encoded
   * @return the encoded String
   */
  public static String encodeURIComponent(String s)
  {
    String result = null;

    try
    {
      result = URLEncoder.encode(s, "UTF-8")
                         .replaceAll("\\+", "%20")
                         .replaceAll("\\%21", "!")
                         .replaceAll("\\%27", "'")
                         .replaceAll("\\%28", "(")
                         .replaceAll("\\%29", ")")
                         .replaceAll("\\%7E", "~");
    }

    // This exception should never occur.
    catch (UnsupportedEncodingException e)
    {
      result = s;
    }

    return result;
  }  

  /**
   * Private constructor to prevent this class from being instantiated.
   */
  private EncodingUtil()
  {
    super();
  }
}
John Topley
fuente
5
Añadiendo una propina. En Android 4.4 encontré que también necesitamos reemplazar, lo %0Aque significa una tecla de retorno en la entrada de Android, o bloqueará el js.
Aloong
¿
Cubre
1
@Aloong ¿A qué te refieres con reemplazar "%0A"? ¿Qué personaje sería el reemplazo? ¿Es solo una cuerda vacía ""?
HendraWD
15

Usando el motor javascript que se envía con Java 6:


import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Wow
{
    public static void main(String[] args) throws Exception
    {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        engine.eval("print(encodeURIComponent('\"A\" B ± \"'))");
    }
}

Salida:% 22A% 22% 20B% 20% c2% b1% 20% 22

El caso es diferente pero se acerca más a lo que quieres.

Ravi Wallau
fuente
Ah, lo siento ... ¡Debería haber mencionado en la pregunta que estoy en Java 1.4 moviéndome a Java 5 en breve!
John Topley
3
Si javascript es la única solución, puede probar Rhino, pero es demasiado para este pequeño problema.
Ravi Wallau
3
Incluso si estaba usando Java 6, creo que esta solución es MUY exagerada. No creo que esté buscando una forma de invocar directamente el método javascript, solo una forma de emularlo.
Programador fuera de la ley
1
Tal vez. Creo que la solución más fácil sería escribir su propia función de escape si no puede encontrar nada que le sirva. Simplemente copie algún método de la clase StringEscapeUtils (Jakarta Commons Lang) y vuelva a implementarlo con sus necesidades.
Ravi Wallau
2
Esto realmente funciona, y si no te preocupa el rendimiento ... creo que es bueno.
2rs2ts
8

Yo uso java.net.URI#getRawPath(), por ejemplo

String s = "a+b c.html";
String fixed = new URI(null, null, s, null).getRawPath();

El valor de fixedserá a+b%20c.html, que es lo que quieres.

El posprocesamiento de la salida URLEncoder.encode()borrará las ventajas que se supone que están en el URI. Por ejemplo

URLEncoder.encode("a+b c.html").replaceAll("\\+", "%20");

le dará a%20b%20c.html, que se interpretará como a b c.html.

Chris Nitchie
fuente
Después de pensar que esta debería ser la mejor respuesta, lo probé en la práctica con algunos nombres de archivo y falló en al menos dos, uno con caracteres cirílicos. Entonces, no, esto obviamente no se ha probado lo suficientemente bien.
AsGoodAsItGets
no funciona para cadenas como:, http://a+b c.htmlarrojará un error
balazs
5

Se me ocurrió mi propia versión del encodeURIComponent, porque la solución publicada tiene un problema, si había un + presente en el String, que debería estar codificado, se convertirá en un espacio.

Entonces aquí está mi clase:

import java.io.UnsupportedEncodingException;
import java.util.BitSet;

public final class EscapeUtils
{
    /** used for the encodeURIComponent function */
    private static final BitSet dontNeedEncoding;

    static
    {
        dontNeedEncoding = new BitSet(256);

        // a-z
        for (int i = 97; i <= 122; ++i)
        {
            dontNeedEncoding.set(i);
        }
        // A-Z
        for (int i = 65; i <= 90; ++i)
        {
            dontNeedEncoding.set(i);
        }
        // 0-9
        for (int i = 48; i <= 57; ++i)
        {
            dontNeedEncoding.set(i);
        }

        // '()*
        for (int i = 39; i <= 42; ++i)
        {
            dontNeedEncoding.set(i);
        }
        dontNeedEncoding.set(33); // !
        dontNeedEncoding.set(45); // -
        dontNeedEncoding.set(46); // .
        dontNeedEncoding.set(95); // _
        dontNeedEncoding.set(126); // ~
    }

    /**
     * A Utility class should not be instantiated.
     */
    private EscapeUtils()
    {

    }

    /**
     * Escapes all characters except the following: alphabetic, decimal digits, - _ . ! ~ * ' ( )
     * 
     * @param input
     *            A component of a URI
     * @return the escaped URI component
     */
    public static String encodeURIComponent(String input)
    {
        if (input == null)
        {
            return input;
        }

        StringBuilder filtered = new StringBuilder(input.length());
        char c;
        for (int i = 0; i < input.length(); ++i)
        {
            c = input.charAt(i);
            if (dontNeedEncoding.get(c))
            {
                filtered.append(c);
            }
            else
            {
                final byte[] b = charToBytesUTF(c);

                for (int j = 0; j < b.length; ++j)
                {
                    filtered.append('%');
                    filtered.append("0123456789ABCDEF".charAt(b[j] >> 4 & 0xF));
                    filtered.append("0123456789ABCDEF".charAt(b[j] & 0xF));
                }
            }
        }
        return filtered.toString();
    }

    private static byte[] charToBytesUTF(char c)
    {
        try
        {
            return new String(new char[] { c }).getBytes("UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return new byte[] { (byte) c };
        }
    }
}
Joe Mill
fuente
¡Gracias por una buena solución! Los otros parecen totalmente ... ineficientes, en mi opinión. Quizás sería incluso mejor sin BitSet en el hardware actual. O dos largos codificados de forma rígida para 0 ... 127.
Jonas N
URLEncoder.encode("+", "UTF-8");yields "%2B", que es la codificación de URL adecuada, por lo que su solución es, mis disculpas, totalmente innecesaria. Por qué diablos URLEncoder.encodeno convierte los espacios en algo %20más allá de mí.
2rs2ts
1

He utilizado con éxito la clase java.net.URI así:

public static String uriEncode(String string) {
    String result = string;
    if (null != string) {
        try {
            String scheme = null;
            String ssp = string;
            int es = string.indexOf(':');
            if (es > 0) {
                scheme = string.substring(0, es);
                ssp = string.substring(es + 1);
            }
            result = (new URI(scheme, ssp, null)).toString();
        } catch (URISyntaxException usex) {
            // ignore and use string that has syntax error
        }
    }
    return result;
}
Mike Bryant
fuente
No, no es completamente exitoso este enfoque, pero está relativamente bien. Sin embargo, todavía tienes problemas. Por ejemplo, el carácter cardinal # java se codificará en% 23 javascript no lo codificará. Ver: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Javascript no espace. AZ az 0-9; , /? : @ & = + $ - _. ! ~ * '() # Y para algunos de estos java se espace.
99Sono
Lo bueno es hacer una prueba UNIT con la siguiente expresión: '' 'String charactersJavascriptDoesNotEspace = "A-Za-z0-9;, /?: @ & = + $ -_.! ~ *' () #"; '' 'el cardenal es el único valor atípico. Por lo tanto, arreglar el algoritmo anterior para hacerlo compatible con JavaScript es trivial.
99Sono
1

Este es un ejemplo sencillo de la solución de Ravi Wallau:

public String buildSafeURL(String partialURL, String documentName)
        throws ScriptException {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    ScriptEngine scriptEngine = scriptEngineManager
            .getEngineByName("JavaScript");

    String urlSafeDocumentName = String.valueOf(scriptEngine
            .eval("encodeURIComponent('" + documentName + "')"));
    String safeURL = partialURL + urlSafeDocumentName;

    return safeURL;
}

public static void main(String[] args) {
    EncodeURIComponentDemo demo = new EncodeURIComponentDemo();
    String partialURL = "https://www.website.com/document/";
    String documentName = "Tom & Jerry Manuscript.pdf";

    try {
        System.out.println(demo.buildSafeURL(partialURL, documentName));
    } catch (ScriptException se) {
        se.printStackTrace();
    }
}

Salida: https://www.website.com/document/Tom%20%26%20Jerry%20Manuscript.pdf

También responde a la pregunta pendiente en los comentarios de Loren Shqipognja sobre cómo pasar una variable String a encodeURIComponent(). El método scriptEngine.eval()devuelve un Object, por lo que se puede convertir a String a través de String.valueOf()otros métodos.

plata
fuente
1

para mí esto funcionó:

import org.apache.http.client.utils.URIBuilder;

String encodedString = new URIBuilder()
  .setParameter("i", stringToEncode)
  .build()
  .getRawQuery() // output: i=encodedString
  .substring(2);

o con un UriBuilder diferente

import javax.ws.rs.core.UriBuilder;

String encodedString = UriBuilder.fromPath("")
  .queryParam("i", stringToEncode)
  .toString()   // output: ?i=encodedString
  .substring(3);

En mi opinión, usar una biblioteca estándar es una mejor idea que el procesamiento posterior manualmente. También la respuesta de @Chris se veía bien, pero no funciona para URL, como " http: // a + b c.html"

balazs
fuente
1
Usar la biblioteca estándar es bueno ... ... a menos que sea un software intermedio y dependa de una versión diferente de una biblioteca estándar, y luego cualquiera que use su código tiene que jugar con las dependencias y luego esperar que nada se rompa ...
Ajax
Sería genial si esta solución funcionara, pero no se comporta de la misma manera que la solicitud encodeURIComponent. encodeURIComponentregresa por ?& el resultado %3F%26%20, pero su sugerencia regresa %3F%26+. Sé que esto se menciona varias veces en otras preguntas y respuestas, pero debería mencionarse aquí, antes de que la gente confíe ciegamente en él.
Philipp
1

Esto es lo que estoy usando:

private static final String HEX = "0123456789ABCDEF";

public static String encodeURIComponent(String str) {
    if (str == null) return null;

    byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
    StringBuilder builder = new StringBuilder(bytes.length);

    for (byte c : bytes) {
        if (c >= 'a' ? c <= 'z' || c == '~' :
            c >= 'A' ? c <= 'Z' || c == '_' :
            c >= '0' ? c <= '9' :  c == '-' || c == '.')
            builder.append((char)c);
        else
            builder.append('%')
                   .append(HEX.charAt(c >> 4 & 0xf))
                   .append(HEX.charAt(c & 0xf));
    }

    return builder.toString();
}

Va más allá de Javascript mediante la codificación porcentual de cada carácter que no es un carácter sin reservas de acuerdo con RFC 3986 .


Esta es la conversión opuesta:

public static String decodeURIComponent(String str) {
    if (str == null) return null;

    int length = str.length();
    byte[] bytes = new byte[length / 3];
    StringBuilder builder = new StringBuilder(length);

    for (int i = 0; i < length; ) {
        char c = str.charAt(i);
        if (c != '%') {
            builder.append(c);
            i += 1;
        } else {
            int j = 0;
            do {
                char h = str.charAt(i + 1);
                char l = str.charAt(i + 2);
                i += 3;

                h -= '0';
                if (h >= 10) {
                    h |= ' ';
                    h -= 'a' - '0';
                    if (h >= 6) throw new IllegalArgumentException();
                    h += 10;
                }

                l -= '0';
                if (l >= 10) {
                    l |= ' ';
                    l -= 'a' - '0';
                    if (l >= 6) throw new IllegalArgumentException();
                    l += 10;
                }

                bytes[j++] = (byte)(h << 4 | l);
                if (i >= length) break;
                c = str.charAt(i);
            } while (c == '%');
            builder.append(new String(bytes, 0, j, UTF_8));
        }
    }

    return builder.toString();
}
Nuno Cruces
fuente
0

La biblioteca de guayaba tiene PercentEscaper:

Escaper percentEscaper = new PercentEscaper("-_.*", false);

"-_. *" son caracteres seguros

false dice PercentEscaper para escapar del espacio con '% 20', no '+'

Aliaksei Nikuliak
fuente
0

Solía String encodedUrl = new URI(null, url, null).toASCIIString(); codificar URL. Para agregar parámetros después de los existentes en el urlusoUriComponentsBuilder

AlexN
fuente