Accueil > Non classé > Encoder correctement une URL en Java

Encoder correctement une URL en Java

Introduction

 

Lors du développement d’une application, on est parfois amené à construire manuellement une URL pour faire appel à un service web donné. Il y a toujours un collègue bien avisé qui vous demande si vous avez “correctement encodé” l’URL en question en insistant sur le “correctement”. Si vous restez perplexe devant cette interrogation alors cet article peut vous donner quelques éléments de réponse. Si vous êtes confiants, lisez-le quand même : la vie est pleine de surprises et ce n’est jamais que 10 minutes de perdues !

 

Problématique

 

La finalité de cet article est de répondre à la question suivante : comment encoder correctement cette URL “complexe et farfelue” en Java :

url exemple

 

J’essaierai de reproduire – à quelques détails près – ma démarche lorsque j’ai eu à répondre à cette question afin de mettre en évidence les pièges à éviter.

 

Un peu de théorie pour commencer…

 

Qu’est ce qu’une URL ?

 

Pour faire court une URL (Uniform Resource Locator) est ce qui permet de localiser une ressource sur le web. Les différentes syntaxes d’une URL sont très bien décrites dans la RFC 3986 et dépassent largement le cadre de cet article. Dans celui-ci, nous allons surtout nous intéresser aux URLs HTTP (ou HTTPS)  dont une des syntaxes est la suivante :

http://<host>/<path/to/resource>/?<query>

avec :

  • <host> : nom de domaine ;
  • <path/to/resource>: chemin de la ressource demandée ;
  • <query> : paramètres supplémentaires rattachés à la requête.

L’URL suivante est un exemple simple de la syntaxe décrite ci-dessus :

URL Simple

Notre URL de départ est quant à elle légèrement plus complexe car elle contient des caractères “étranges” (e.g un slash dans un nom de paramètre, etc). On voudrait la décomposer de la sorte :

urlCommentee

 

Pourquoi encoder une URL ?

 

D’après la RFC 3986, seuls les caractères ASCII ainsi que certains caractères réservés à la syntaxe d’une URL (par exemple le “/” ou le “?”) peuvent être utilisés. Ainsi les caractères non ASCII (e.g :  “é” par exemple) doivent être encodés. De même si on souhaite utiliser un caractère réservé comme valeur, celui-ci doit être encodé (on dit parfois “échappé”), afin de lui retirer sa dimension syntaxique.

Dans notre exemple le caractère “?” est utilisé dans le Path2 et ne devrait pas être considéré comme le délimiteur du début de la Query : il doit alors être encodé. Il en est de même pour les caractères “&” et “=” dans la valeur du paramètre de la Query. 

Comment encoder une URL ?

 

Pour encoder un caractère, il faut tout d’abord choisir un encodage. La spécification n’est pas précise à ce sujet et l’utilisateur peut en théorie choisir l’encodage qui lui convient s’il est sûr que l’URL sera décodée avec le même encodage côté serveur. Ceci étant dit, les encodages les plus utilisés restent l’ISO-8859 et l’UTF-8.

Il suffit alors de récupérer la représentation en hexadécimal du caractère dans l’encodage choisi  et de le préfixer par “%”. Par exemple le caractère “?” est représenté par 3F en UTF-8 : son encodage est donc %3F. Ceux qui se demandaient l’origine de certaines URLs truffées de “%”, savent désormais qu’il s’agit d’URLs encodées. À noter que les navigateurs récents masquent pour la plupart cette encodage et présentent directement la version décodée pour ne pas dérouter l’utilisateur.

Après avoir pris connaissance de ces informations, on peut en déduire que notre fameuse URL devrait prendre la forme suivante une fois encodée en UTF-8 :

URL Encodee

 

 

…Et en pratique, ça se passe comment ?

 

La classe java.net.URL

 

À première vue, la classe java.net.URL peut sembler faire l’affaire. Dans les sections qui suivent, je vais présenter les résultats d’utilisation d’une classe sous forme de tests unitaires. Ces derniers sont toujours “en succès” car ils ne présentent pas le résultat souhaité mais le résultat réellement obtenu. Il s’agit d’une utilisation détournée pour éviter les System.out.println() et les commentaires qui vont avec.  Un exemple valant mieux qu’un long discours, voyons ce que cela donne sur notre classe java.net.URL :


public void testEncodeWithURL() throws MalformedURLException {
    URL url = new URL("http://www.excilys.com/pa/t=?h/?pa/ram=va&l==ue");
    assertEquals("/pa/t=", url.getPath()); // il manque le "?h"
    assertEquals("h/?pa/ram=va&l==ue", url.getQuery()); // il y a un "h/?" en trop!
}

La classe URL semble n’effectuer aucun encodage sur la chaîne de caractères passée en paramètre de son constructeur. Ceci est confirmée par la Javadoc de la classe :

The URL class does not itself encode or decode any URL component
according to the escaping mechanism defined in RFC2396

De toutes les manières, on voit très mal comment la classe URL aurait pu interpréter toute seule le premier caractère “?” comme faisant parti du path et non comme délimiteur des paramètres de requête.

Une première règle émerge de tout cela : Il est impossible d’encoder une URL à partir d’une simple chaîne de caractères. Il faut obligatoirement encoder chaque portion d’URL séparément. A ce stade, si on devait imaginer un encodeur “idéal”, celui-ci devrait avoir le comportement suivant :


Encoder encoder = new Encoder();
String chemin = encoder.encode("pa/t=?h");
String nomParam = encoder.encode("pa/ram");
String valeurParam = encoder.encode("va&l==ue");
URL url = new URL("http://www.excilys.com/"+chemin+"/?"+nomParam+"="+valeurParam;

La classe java.net.URLEncoder

 

Armés de la précédente règle et après une rapide recherche dans le JDK, nous tombons sur la classe java.net.URLEncoder dont le nom explicite semble indiquer qu’elle “encode une URL”. De plus son API est très proche de celle de l’encodeur idéal décrit plus haut. Ci-dessous les résultats de son utilisation :


public void testEncodeWithURLEncoder() throws UnsupportedEncodingException {
    String encoding = "UTF-8";
    String chemin = URLEncoder.encode("pa/t=?h", encoding);
    String nomParam = URLEncoder.encode("pa/ram", encoding);
    String valeurParam = URLEncoder.encode("va&l==ue", encoding);
    String encodedURL = "http://www.excilys.com/" + chemin + "/?" + nomParam + "=" + valeurParam;
    String result = "http://www.excilys.com/pa%2Ft%3D%3Fh/?pa%2Fram=va%26l%3D%3Due";
    assertEquals(result, encodedURL);
}

Plusieurs remarques découlent de ce test. Tout d’abord, l’API proposée par URLEncoder s’avère être peu pratique pour construire des URLs complexes. En effet, son utilisation requiert de nombreuses concaténations qui ont pour conséquence un code peu lisible.

Deuxièmement et en regardant de plus près, on constate qu’URLEncoder encode plus que nécessaire. En effet d’après la spécification sur les URLs, il n’est nulle besoin d’encoder le caractère “=” lorsque celui-ci est compris dans le Path. Tout comme il est inutile d’encoder le caractère “/” dans la partie Query. En somme, il n’existe pas un unique jeu de caractères réservés pour les URLs mais des jeux différents en fonction de la nature de la portion d’URL.

En prenant un autre exemple, le caractère “&” a un sens dans la partie Query car il permet de délimiter les différents paramètres mais n’en possède aucun lorsqu’il est présent dans la partie Path. Si on veut l’utiliser comme valeur il doit être encodé dans le premier cas de figure et laissé comme tel dans le second. 

On rajoute donc cette connaissance à notre boite à outils : il ne suffit pas de parcourir séquentiellement les caractères d’une URL pour échapper les caractères réservés (comme le fait URLEncoder) . Il faut obligatoirement préciser la nature de la portion d’URL à encoder. On enrichit alors l’API de notre encodeur idéal de la manière suivante :


Encoder encoder = new Encoder();
encoder.encodePath("pa/t=?h");
encoder.addQueryParam("pa/ram","va&l==ue");
...
URL url = encoder.buildURL();

Enfin et en regardant de plus près la Javadoc de la “mal nommée” classe URLEncoder, il semblerait que son but initial n’est pas d’encoder une URL mais d’encoder une chaîne de caractères en provenance d’un formulaire HTML au format application/x-www-form-urlencoded : 

Utility class for HTML form encoding. This class contains static methods for converting a String to the application/x-www-form-urlencoded MIME format.

Enfin la classe javax.ws.rs.core.UriBuilder

 

Après avoir éliminé URLEncoder et après une recherche plus approfondie dans le JDK, il s’est avéré qu’aucune classe standard ne réponde réellement à notre besoin. Cependant il semblerait que la spécification JAX-RS (Java API for RESTFul Services) définisse une classe abstraite : javax.ws.rs.UriBuilder dédiée à la construction et l’encodage d’un URI (et a fortiori d’une URL). L’implémentation de référence de JAX-RS se nomme Jersey mais pour profiter uniquement de l’implémentation de UriBuilder, il suffit d’importer le module jersey-client  :


<dependency>
 <groupId>com.sun.jersey</groupId>
 <artifactId>jersey-client</artifactId>
 <version>1.17.1</version>
</dependency>

L’utilisation de cette classe est illustrée ci-dessous :


public void testWithUriBuilder() {
    UriBuilder builder = new UriBuilderImpl();
    builder.scheme("http");
    builder.host("www.excilys.com");
    builder.path("pat=?h");
    builder.queryParam("pa/ram", "va&l==ue");
    String encodedURL = builder.build().toString();
    String result = "http://www.excilys.com/pat=%3Fh?pa/ram=va%26l%3D%3Due";
    assertEquals(result, encodedURL);
}

Comme vous pouvez le constater, l’encodage des caractères a été correctement réalisé selon la nature de la portion d’URL. On peut en effet constater que le caractère “=”  présent dans le Path n’a pas été encodé tout comme le “/” dans la Query.

De plus l’API est simple et expressive rendant le code plus agréable à lire.

 

Nous tenons notre classe gagnante! ;-)

 

Conclusion

 

Nous ne sommes évidemment pas limités à l’utilisation de javax.ws.rs.core.UriBuilder pour encoder nos URLs. On peut utiliser d’autres classes utilitaires du même genre (à creuser du coté d’Apache), ou même coder sa propre classe. Quelle que soit la solution retenue, il faut bien faire attention aux critères suivants :

  • Encoder chaque portion d’URL séparément;
  • Prendre en compte le fait que chaque portion d’URL possède son propre jeu de caractères réservés;
  • Avoir une API simple.
Share
  1. 06/03/2015 à 16:53 | #1

    Avoid Enum a été enlevé de la doc. C’est donc correct à utiliser maintenant! :)

  2. 13/03/2015 à 18:09 | #2

    Un article très pertinent

  3. 02/08/2017 à 06:44 | #3

    Thank you

  1. 13/03/2015 à 12:55 | #1
  2. 02/08/2015 à 15:07 | #2