Java setara dengan encodeURIComponent JavaScript yang menghasilkan keluaran identik?

92

Saya telah bereksperimen dengan berbagai bit kode Java mencoba menghasilkan sesuatu yang akan menyandikan string yang berisi tanda kutip, spasi dan karakter Unicode "eksotis" dan menghasilkan keluaran yang identik dengan fungsi encodeURIComponent JavaScript .

String uji penyiksaan saya adalah: "A" B ± "

Jika saya memasukkan pernyataan JavaScript berikut di Firebug:

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

—Lalu saya mendapatkan:

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

Inilah program Java tes kecil saya:

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"));
  }
}

—Program ini menghasilkan:

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

Dekat, tapi tidak ada cerutu! Apa cara terbaik untuk menyandikan string UTF-8 menggunakan Java sehingga menghasilkan keluaran yang sama dengan JavaScript encodeURIComponent?

EDIT: Saya menggunakan Java 1.4 segera pindah ke Java 5.

John Topley
sumber

Jawaban:

63

Melihat perbedaan implementasi, saya melihat bahwa:

MDC padaencodeURIComponent() :

  • karakter literal (representasi regex): [-a-zA-Z0-9._*~'()!]

Dokumentasi Java 1.5.0 tentangURLEncoder :

  • karakter literal (representasi regex): [-a-zA-Z0-9._*]
  • karakter spasi " "diubah menjadi tanda plus "+".

Jadi pada dasarnya, untuk mendapatkan hasil yang diinginkan, gunakan URLEncoder.encode(s, "UTF-8")dan kemudian lakukan beberapa proses pasca:

  • ganti semua kemunculan "+"dengan"%20"
  • mengganti semua kemunculan "%xx"mewakili salah satu dari [~'()!]kembali ke bagian literal mereka
Tomalak
sumber
Saya berharap Anda telah menulis "Ganti semua kemunculan"% xx "yang mewakili salah satu dari [~ '()!] Kembali ke bagian literal mereka" dalam beberapa bahasa sederhana. :( kepala
mungilku
1
@Shailendra [~'()!]artinya "~"atau "'"atau "("atau ")"atau "!". :) Saya juga merekomendasikan untuk mempelajari dasar-dasar regex. (Saya juga tidak memperluasnya karena setidaknya dua jawaban lain menunjukkan kode Java masing-masing.)
Tomalak
3
Mengganti semua kemunculan "+"dengan "%20"berpotensi merusak, seperti "+"karakter legal di jalur URI (meskipun tidak dalam string kueri). Misalnya, "a + b c" harus dikodekan sebagai "a+b%20c"; solusi ini akan mengubahnya menjadi "a%20b%20c". Sebagai gantinya, gunakan new URI(null, null, value, null).getRawPath().
Chris Nitchie
@ChrisNitchie Bukan itu inti dari pertanyaannya. Pertanyaannya adalah "Java setara dengan encodeURIComponent JavaScript yang menghasilkan keluaran identik?" , bukan "Fungsi komponen-URI-encode Java Generik?" .
Tomalak
118

Ini adalah kelas yang saya dapatkan pada akhirnya:

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
sumber
5
Menambahkan tip. Di Android 4.4 saya menemukan bahwa kita juga perlu mengganti %0Ayang berarti tombol kembali di input Android, atau akan membuat js crash.
Aloong
Apakah Anda membahas semuanya di sini: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
kamaci
1
@Aloong Apa yang Anda maksud dengan mengganti "%0A"? Karakter apa yang akan menjadi penggantinya? Apakah itu hanya string kosong ""?
HendraWD
15

Menggunakan mesin javascript yang dikirimkan dengan 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 ± \"'))");
    }
}

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

Kasusnya berbeda tetapi lebih dekat dengan apa yang Anda inginkan.

Ravi Wallau
sumber
Ah, maaf ... Seharusnya saya menyebutkan dalam pertanyaan bahwa saya di Java 1.4 akan segera pindah ke Java 5!
John Topley
3
Jika javascript adalah satu-satunya solusi, Anda dapat mencoba Rhino, tetapi itu terlalu berlebihan hanya untuk masalah kecil ini.
Ravi Wallau
3
Bahkan jika dia menggunakan Java 6, saya pikir solusi ini JAUH di atas. Saya tidak berpikir dia sedang mencari cara untuk secara langsung memanggil metode javascript, hanya cara untuk menirunya.
Outlaw Programmer
1
Mungkin. Saya pikir solusi termudah adalah menulis fungsi pelarian Anda sendiri jika Anda tidak dapat menemukan apa pun yang melakukan trik untuk Anda. Cukup salin beberapa metode dari kelas StringEscapeUtils (Jakarta Commons Lang) dan terapkan kembali dengan kebutuhan Anda.
Ravi Wallau
2
Ini benar-benar berfungsi, dan jika Anda tidak khawatir tentang kinerja ... Saya pikir itu bagus.
2rs2t
8

Saya menggunakan java.net.URI#getRawPath(), misalnya

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

Nilai fixedkemauan a+b%20c.html, yang Anda inginkan.

Pasca-pemrosesan, output dari URLEncoder.encode()akan menghapus nilai tambah apa pun yang seharusnya ada di URI. Sebagai contoh

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

akan memberi Anda a%20b%20c.html, yang akan diartikan sebagai a b c.html.

Chris Nitchie
sumber
Setelah berpikir ini harus menjadi jawaban terbaik, saya mencobanya dalam praktik dengan beberapa nama file, dan gagal setidaknya dalam dua, satu dengan karakter cyrillic. Jadi, tidak, ini jelas belum diuji dengan cukup baik.
AsGoodAsItGets
tidak berfungsi untuk string seperti:, http://a+b c.htmlitu akan membuat kesalahan
:, balazs
5

Saya datang dengan versi encodeURIComponent saya sendiri, karena solusi yang diposting memiliki satu masalah, jika ada + hadir dalam String, yang harus dikodekan, itu akan diubah menjadi spasi.

Jadi inilah kelasku:

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
sumber
Terima kasih untuk solusi yang bagus! Yang lain terlihat sangat ... tidak efisien, IMO. Mungkin akan lebih baik tanpa BitSet pada perangkat keras saat ini. Atau dua kerinduan keras untuk 0 ... 127.
Jonas N
URLEncoder.encode("+", "UTF-8");hasil "%2B", yang merupakan pengkodean URL yang tepat, jadi solusi Anda, maaf, sama sekali tidak perlu. Mengapa di bumi URLEncoder.encodetidak mengubah ruang menjadi %20berada di luar jangkauan saya.
2rs2t
1

Saya telah berhasil menggunakan kelas java.net.URI seperti:

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
sumber
Tidak, ini tidak sepenuhnya berhasil dengan pendekatan ini, tetapi relatif baik. Anda masih memiliki masalah. Misalnya karakter utama # java akan disandikan ke% 23 javascript tidak akan menyandikannya. Lihat: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Javascript tidak mendukung. AZ az 0-9; , /? : @ & = + $ - _. ! ~ * '() # Dan untuk beberapa java ini akan espace.
99Sono
Hal yang baik dengan membuat tes UNIT dengan ekspresi berikut: '' 'Karakter stringJavascriptDoesNotEspace = "A-Za-z0-9;, /?: @ & = + $ -_.! ~ *' () #"; '' 'Kardinal adalah satu-satunya pencilan. Jadi memperbaiki algoritma di atas agar kompatibel dengan javascript itu sepele.
99Sono
1

Ini adalah contoh langsung solusi 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();
    }
}

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

Ini juga menjawab pertanyaan gantung di komentar oleh Loren Shqipognja tentang cara meneruskan variabel String ke encodeURIComponent(). Metode ini scriptEngine.eval()mengembalikan Object, sehingga dapat dikonversi ke String melalui di String.valueOf()antara metode lainnya.

perak
sumber
1

bagi saya ini berhasil:

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

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

atau dengan UriBuilder yang berbeda

import javax.ws.rs.core.UriBuilder;

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

Menurut pendapat saya, menggunakan pustaka standar adalah ide yang lebih baik daripada pemrosesan pos secara manual. Juga jawaban @Chris tampak bagus, tetapi tidak berfungsi untuk url, seperti " http: // a + b c.html"

balazs
sumber
1
Menggunakan pustaka standar itu bagus ... ... kecuali Anda adalah perangkat menengah, dan bergantung pada versi berbeda dari pustaka standar, dan kemudian siapa pun yang menggunakan kode Anda harus mengutak-atik dependensi, dan kemudian berharap tidak ada yang rusak ...
Ajax
Akan lebih bagus jika solusi ini berhasil, tetapi tidak berperilaku dengan cara yang sama seperti permintaan encodeURIComponent. encodeURIComponentkembali untuk ?& hasilnya %3F%26%20, tetapi saran Anda kembali %3F%26+. Saya tahu ini disebutkan beberapa kali dalam pertanyaan dan jawaban lain, tetapi harus disebutkan di sini, sebelum orang mempercayainya.
Philipp
1

Inilah yang saya gunakan:

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();
}

Ini melampaui Javascript dengan persen-encoding setiap karakter yang bukan merupakan karakter yang tidak dicadangkan menurut RFC 3986 .


Ini adalah konversi yang berlawanan:

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
sumber
0

Perpustakaan jambu biji memiliki PercentEscaper:

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

"-_. *" adalah karakter aman

false mengatakan PercentEscaper untuk keluar dari spasi dengan '% 20', bukan '+'

Aliaksei Nikuliak
sumber
0

Saya biasa String encodedUrl = new URI(null, url, null).toASCIIString(); menyandikan url. Untuk menambah parameter setelah yang ada di urlsaya gunakanUriComponentsBuilder

AlexN
sumber