95

Do you know of any utility class/library, that can convert Map into URL-friendly query string?

Example:

I have a map:

"param1"=12,
"param2"="cat"

I want to get:

param1=12&param2=cat

final output

relativeUrl+param1=12&param2=cat
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
Ula Krukar
  • 12,549
  • 20
  • 51
  • 65

20 Answers20

73

The most robust one I saw off-the-shelf is the URLEncodedUtils class from Apache Http Compoments (HttpClient 4.0).

The method URLEncodedUtils.format() is what you need.

It doesn't use map so you can have duplicate parameter names, like,

  a=1&a=2&b=3

Not that I recommend this kind of use of parameter names.

Simeon Leyzerzon
  • 18,658
  • 9
  • 54
  • 82
ZZ Coder
  • 74,484
  • 29
  • 137
  • 169
  • 3
    It's close to what I need. The only problem is it requires NameValuePair objects and what I have are Map.Entry objects at best. – Ula Krukar May 11 '10 at 12:05
  • 1
    @Ula: You could use this and do `Collections2.transform(paramMap.entrySet(), function)` where function takes a Map.Entry and returns BasicNameValuePair. (Or do the same in plain old Java without Google Collections.) Granted, about 6 extra lines of own code needed. – Jonik May 11 '10 at 12:33
  • 3
    @Jonik, that is exactly what I will do :) But still I am surprised that there is nothing that simply takes a Map, seems so obvious that there should. There is lots of methods that turn query string into a map and nothing that does the opposite. – Ula Krukar May 11 '10 at 12:40
  • 13
    @UlaKrukar, it's because a map is the wrong data structure to use. It doesn't preserve order and it doesn't allow duplicate keys. – James Moore Oct 29 '11 at 21:58
  • 3
    LinkedHashMap could work good enough ... URIBuilder from Groovy accepts Map interface to generate queryString – Rafael Nov 19 '14 at 16:39
  • Having duplicate keys is important for certain kinds of form inputs. I've seen it used to encode an array of values for the key. – jpaugh Jul 11 '16 at 18:03
  • 1
    Please notice that this function replaces space with `+`, not with `%20` – Grief Aug 26 '16 at 14:30
  • There is an error, where is `URLEncodeUtils.format()` should be `URLEncodedUtils.format()`. There is a `d` missing. – rigon May 11 '17 at 19:57
  • @JamesMoore the order is not relevant in almost every case, and it's actually `Map`. – user2914191 Sep 25 '17 at 00:10
  • @user2914191 - it was six years ago, but I think I read UlaKrukar's comment as wanting a map from a string to a single string, instead of the more correct - but ever-so-slightly harder to use - string to array of strings. And library writers have to assume that order is in fact important. (I suspect ordering is a bit more common than you might think, since we're talking about things used by APIs, not just browsers - but it's probably not common.) – James Moore Sep 25 '17 at 23:26
  • 1
    @JamesMoore order can be preserved by using `LinkedHashMap` if im not mistaken. after 6 years youd think the util would transform `Map` into URL encoded string – user2914191 Sep 27 '17 at 00:08
50

Here's something that I quickly wrote; I'm sure it can be improved upon.

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

public class MapQuery {
    static String urlEncodeUTF8(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedOperationException(e);
        }
    }
    static String urlEncodeUTF8(Map<?,?> map) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<?,?> entry : map.entrySet()) {
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(String.format("%s=%s",
                urlEncodeUTF8(entry.getKey().toString()),
                urlEncodeUTF8(entry.getValue().toString())
            ));
        }
        return sb.toString();       
    }
    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("p1", 12);
        map.put("p2", "cat");
        map.put("p3", "a & b");         
        System.out.println(urlEncodeUTF8(map));
        // prints "p3=a+%26+b&p2=cat&p1=12"
    }
}
polygenelubricants
  • 376,812
  • 128
  • 561
  • 623
  • 13
    Writing your own isn't difficult, the question is where, if anywhere, is there an off-the-shelf version. – skaffman May 11 '10 at 11:19
  • @ZZCoder: Yeah, I was wondering about that. Anything else? I'll do mass update on next revision based on suggestions. – polygenelubricants May 11 '10 at 11:19
  • 1
    @skaffman: Yeah, I saw that later after I wrote it, but anyway, now that I already did, I might as well use this opportunity to have other people review my code and improve it etc. If not for OP, it'll still benefit me, and perhaps some others too. – polygenelubricants May 11 '10 at 11:21
  • 5
    The parametermap is **usually** represented as `Map>` or `Map`. You can namely have multiple values for the same name. Not sure if OP was smart to design it that way. – BalusC May 11 '10 at 12:21
  • @BalusC: thanks for info. By the way, how should this (if it were written for a library) handle `null` keys/values? Right now the `toString()` causes `NullPointerException`, and I justify it by saying "fail-fast", but I'm not sure how that would go with people who just wants things to "work". – polygenelubricants May 11 '10 at 12:31
  • Only values are allowed to be empty (I'd insert a `""`). NPE's on keys however, I would just document it and let them go. It's developer's fault. – BalusC May 11 '10 at 12:38
  • Might consider using a LinkedHashMap instead of HashMap because it is nice when an API gives you back the values you entered in the same order you entered them. – Ryan Nov 29 '12 at 17:00
  • Java 10 introduced `URLEncoder.encode(String s, Charset charset)` which doesn't have to deal with `UnsupportedEncodingException` – Johannes Brodwall Dec 05 '21 at 21:54
49

I found a smooth solution using java 8 and polygenelubricants' solution.

parameters.entrySet().stream()
    .map(p -> urlEncodeUTF8(p.getKey()) + "=" + urlEncodeUTF8(p.getValue()))
    .reduce((p1, p2) -> p1 + "&" + p2)
    .orElse("");
eclipse
  • 2,831
  • 3
  • 26
  • 34
24

In Spring Util, there is a better way..,

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("key", key);
params.add("storeId", storeId);
params.add("orderId", orderId);
UriComponents uriComponents =     UriComponentsBuilder.fromHttpUrl("http://spsenthil.com/order").queryParams(params).build();
ListenableFuture<ResponseEntity<String>> responseFuture =     restTemplate.getForEntity(uriComponents.toUriString(), String.class);
Senthil Arumugam SP
  • 1,461
  • 12
  • 17
  • 1
    queryParams is prive :/ – user3364192 Apr 27 '16 at 12:22
  • @user3364192 Use the `UriComponentsBuilder` – MaxZoom Aug 05 '16 at 16:59
  • I tried the above solution with some null values in the MultiValueMap which i wanted to omit before calling `UriComponentsBuilder.fromHttpUrl`. When I added `params.values().removeIf(entry -> entry.getValue() == null);`, it didn't work. Anyone know why i can't remove entries from a MultiValueMap using the above syntax? – Orby Aug 01 '19 at 21:17
  • 1
    This does not answer the OP because the question was how to convert the `Map` into a `String`, not a `UriComponents` object. But this is useful for people who just want to issue the http request. – axiopisty Jan 07 '20 at 21:45
  • I recommend **not** to use `UriComponentsBuilder`: It is overly complicated, it mishandles the URL query values encoding (although intentionally), and doesn't really add much value over just `params.map { "${urlEncode(it.first)}=${urlEncode(it.second)}" }.joinToString("&")` – Ondra Žižka Nov 18 '22 at 20:38
20

Update June 2016

Felt compelled to add an answer having seen far too many SOF answers with dated or inadequate answers to very common problem - a good library and some solid example usage for both parse and format operations.

Use org.apache.httpcomponents.httpclient library. The library contains this org.apache.http.client.utils.URLEncodedUtils class utility.

For example, it is easy to download this dependency from Maven:

 <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5</version>
 </dependency>

For my purposes I only needed to parse (read from query string to name-value pairs) and format (read from name-value pairs to query string) query strings. However, there are equivalents for doing the same with a URI (see commented out line below).

// Required imports

import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

// code snippet

public static void parseAndFormatExample() throws UnsupportedEncodingException {
        final String queryString = "nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk";
        System.out.println(queryString);
        // => nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk

        final List<NameValuePair> params =
                URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
        // List<NameValuePair> params = URLEncodedUtils.parse(new URI(url), "UTF-8");

        for (final NameValuePair param : params) {
            System.out.println(param.getName() + " : " + param.getValue());
            // => nonce : 12345
            // => redirectCallbackUrl : http://www.bbc.co.uk
        }

        final String newQueryStringEncoded =
                URLEncodedUtils.format(params, StandardCharsets.UTF_8);


        // decode when printing to screen
        final String newQueryStringDecoded =
                URLDecoder.decode(newQueryStringEncoded, StandardCharsets.UTF_8.toString());
        System.out.println(newQueryStringDecoded);
        // => nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk
    }

This library did exactly what I needed and was able to replace some hacked custom code.

arcseldon
  • 35,523
  • 17
  • 121
  • 125
10

If you actually want to build a complete URI, try URIBuilder from Apache Http Compoments (HttpClient 4).

This does not actually answer the question, but it answered the one I had when I found this question.

Thraidh
  • 670
  • 5
  • 11
10

I wanted to build on @eclipse's answer using java 8 mapping and reducing.

 protected String formatQueryParams(Map<String, String> params) {
      return params.entrySet().stream()
          .map(p -> p.getKey() + "=" + p.getValue())
          .reduce((p1, p2) -> p1 + "&" + p2)
          .map(s -> "?" + s)
          .orElse("");
  }

The extra map operation takes the reduced string and puts a ? in front only if the string exists.

Mahmoud Abu Alheja
  • 3,343
  • 2
  • 24
  • 41
sbzoom
  • 3,273
  • 4
  • 29
  • 34
  • This is works but mess up with order badly, which isn't a problem in real life except for tests assertThat() for sure will be a pain. – M_F Sep 17 '20 at 13:46
  • 2
    without encoding will produce problems if params value contain special characters – Yu Jiaao Feb 07 '21 at 04:35
  • This answer is not URL-encoding the segments and repeating an already given answer. – BalusC Dec 05 '22 at 11:54
  • This answer was 3 years before the one above. The one above is repeating. – sbzoom Dec 05 '22 at 15:57
8

Another 'one class'/no dependency way of doing it, handling single/multiple:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class UrlQueryString {
  private static final String DEFAULT_ENCODING = "UTF-8";

  public static String buildQueryString(final LinkedHashMap<String, Object> map) {
    try {
      final Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
      final StringBuilder sb = new StringBuilder(map.size() * 8);
      while (it.hasNext()) {
        final Map.Entry<String, Object> entry = it.next();
        final String key = entry.getKey();
        if (key != null) {
          sb.append(URLEncoder.encode(key, DEFAULT_ENCODING));
          sb.append('=');
          final Object value = entry.getValue();
          final String valueAsString = value != null ? URLEncoder.encode(value.toString(), DEFAULT_ENCODING) : "";
          sb.append(valueAsString);
          if (it.hasNext()) {
            sb.append('&');
          }
        } else {
          // Do what you want...for example:
          assert false : String.format("Null key in query map: %s", map.entrySet());
        }
      }
      return sb.toString();
    } catch (final UnsupportedEncodingException e) {
      throw new UnsupportedOperationException(e);
    }
  }

  public static String buildQueryStringMulti(final LinkedHashMap<String, List<Object>> map) {
    try {
      final StringBuilder sb = new StringBuilder(map.size() * 8);
      for (final Iterator<Entry<String, List<Object>>> mapIterator = map.entrySet().iterator(); mapIterator.hasNext();) {
        final Entry<String, List<Object>> entry = mapIterator.next();
        final String key = entry.getKey();
        if (key != null) {
          final String keyEncoded = URLEncoder.encode(key, DEFAULT_ENCODING);
          final List<Object> values = entry.getValue();
          sb.append(keyEncoded);
          sb.append('=');
          if (values != null) {
            for (final Iterator<Object> listIt = values.iterator(); listIt.hasNext();) {
              final Object valueObject = listIt.next();
              sb.append(valueObject != null ? URLEncoder.encode(valueObject.toString(), DEFAULT_ENCODING) : "");
              if (listIt.hasNext()) {
                sb.append('&');
                sb.append(keyEncoded);
                sb.append('=');
              }
            }
          }
          if (mapIterator.hasNext()) {
            sb.append('&');
          }
        } else {
          // Do what you want...for example:
          assert false : String.format("Null key in query map: %s", map.entrySet());
        }
      }
      return sb.toString();
    } catch (final UnsupportedEncodingException e) {
      throw new UnsupportedOperationException(e);
    }
  }

  public static void main(final String[] args) {
    // Examples: could be turned into unit tests ...
    {
      final LinkedHashMap<String, Object> queryItems = new LinkedHashMap<String, Object>();
      queryItems.put("brand", "C&A");
      queryItems.put("count", null);
      queryItems.put("misc", 42);
      final String buildQueryString = buildQueryString(queryItems);
      System.out.println(buildQueryString);
    }
    {
      final LinkedHashMap<String, List<Object>> queryItems = new LinkedHashMap<String, List<Object>>();
      queryItems.put("usernames", new ArrayList<Object>(Arrays.asList(new String[] { "bob", "john" })));
      queryItems.put("nullValue", null);
      queryItems.put("misc", new ArrayList<Object>(Arrays.asList(new Integer[] { 1, 2, 3 })));
      final String buildQueryString = buildQueryStringMulti(queryItems);
      System.out.println(buildQueryString);
    }
  }
}

You may use either simple (easier to write in most cases) or multiple when required. Note that both can be combined by adding an ampersand... If you find any problems let me know in the comments.

Christophe Roussy
  • 16,299
  • 4
  • 85
  • 85
7

This is the solution I implemented, using Java 8 and org.apache.http.client.URLEncodedUtils. It maps the entries of the map into a list of BasicNameValuePair and then uses Apache's URLEncodedUtils to turn that into a query string.

List<BasicNameValuePair> nameValuePairs = params.entrySet().stream()
   .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
   .collect(Collectors.toList());

URLEncodedUtils.format(nameValuePairs, Charset.forName("UTF-8"));
Marlon Bernardes
  • 13,265
  • 7
  • 37
  • 44
  • Worked perfectly! Only had to pull in one dependency: `compile 'org.apache.httpcomponents:httpclient:4.5.2'` – Miguel Reyes May 20 '16 at 17:35
  • Would it be possible for you to explain what is going on in your first block of code that create the nameValuePairs that is passed to the URLEncodedUtils. ? – silversunhunter Aug 17 '17 at 18:50
4

There's nothing built into Java to do this. But, hey, Java is a programming language, so. Let's program it!

map
.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"))

This gives you param1=12&param2=cat. Now we need to join the URL and this bit together. You'd think you can just do: URL + "?" + theAbove but if the URL already contains a question mark, you have to join it all together with "&" instead. One way to check is to see if there's a question mark in the URL someplace already.

Also, I don't quite know what is in your map. If it's raw stuff, you probably have to safeguard the call to e.getKey() and e.getValue() with URLEncoder.encode or similar.

Yet another way to go is that you take a wider view. Are you trying to append a map's content to a URL, or... Are you trying to make an HTTP (S) request from a Java process with the stuff in the map as (additional) HTTP params? In the latter case, you can look into an HTTP library like OkHttp which has some nice APIs to do this job, then you can forego any need to mess about with that URL in the first place.

Dharman
  • 30,962
  • 25
  • 85
  • 135
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
3

Using EntrySet and Streams:

map
  .entrySet()
  .stream()
  .map(e -> e.getKey() + "=" + e.getValue())
  .collect(Collectors.joining("&"));
SIVA KUMAR
  • 105
  • 1
  • 2
2

You can use a Stream for this, but instead of appending query parameters myself I'd use a Uri.Builder. For example:

final Map<String, String> map = new HashMap<>();
map.put("param1", "cat");
map.put("param2", "12");

final Uri uri = 
    map.entrySet().stream().collect(
        () -> Uri.parse("relativeUrl").buildUpon(),
        (builder, e) -> builder.appendQueryParameter(e.getKey(), e.getValue()),
        (b1, b2) -> { throw new UnsupportedOperationException(); }
    ).build();

//Or, if you consider it more readable...
final Uri.Builder builder = Uri.parse("relativeUrl").buildUpon();
map.entrySet().forEach(e -> builder.appendQueryParameter(e.getKey(), e.getValue())
final Uri uri = builder.build();

//...    

assertEquals(Uri.parse("relativeUrl?param1=cat&param2=12"), uri);
PPartisan
  • 8,173
  • 4
  • 29
  • 48
2

Here's a simple kotlin solution:

fun Map<String, String>.toUrlParams(): String =
    entries.joinToString("&") {
        it.key.toUrlEncoded() + "=" + it.value.toUrlEncoded()
    }

fun String.toUrlEncoded(): String = URLEncoder.encode(
    this, StandardCharsets.UTF_8
)
Andras Kloczl
  • 8,415
  • 2
  • 21
  • 23
1

To improve a little bit upon @eclipse's answer: In Javaland a request parameter map is usually represented as a Map<String, String[]>, a Map<String, List<String>> or possibly some kind of MultiValueMap<String, String> which is sort of the same thing. In any case: a parameter can usually have multiple values. A Java 8 solution would therefore be something along these lines:

public String getQueryString(HttpServletRequest request, String encoding) {
    Map<String, String[]> parameters = request.getParameterMap();

    return parameters.entrySet().stream()
            .flatMap(entry -> encodeMultiParameter(entry.getKey(), entry.getValue(), encoding))
            .reduce((param1, param2) -> param1 + "&" + param2)
            .orElse("");
}

private Stream<String> encodeMultiParameter(String key, String[] values, String encoding) {
    return Stream.of(values).map(value -> encodeSingleParameter(key, value, encoding));
}

private String encodeSingleParameter(String key, String value, String encoding) {
    return urlEncode(key, encoding) + "=" + urlEncode(value, encoding);
}

private String urlEncode(String value, String encoding) {
    try {
        return URLEncoder.encode(value, encoding);
    } catch (UnsupportedEncodingException e) {
        throw new IllegalArgumentException("Cannot url encode " + value, e);
    }
}
Auke
  • 534
  • 5
  • 16
1

I make these functions than also send just the property name when the value is null.

public static String urlEncode(Map<?, ?> map) {
  return map.entrySet().stream().map(
      entry -> entry.getValue() == null
        ? urlEncode(entry.getKey())
        : urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue())
    ).collect(Collectors.joining("&"));
}

public static String urlEncode(Object obj) {
  return URLEncoder.encode(obj.toString(), StandardCharsets.UTF_8);
}
Daniel De León
  • 13,196
  • 5
  • 87
  • 72
1

For multivalue map you can do like below (using java 8 stream api's)

Url encoding has been taken cared in this.

MultiValueMap<String, String> params =  new LinkedMultiValueMap<>();
String urlQueryString = params.entrySet()
            .stream()
            .flatMap(stringListEntry -> stringListEntry.getValue()
                    .stream()
                    .map(s -> UriUtils.encode(stringListEntry.getKey(), StandardCharsets.UTF_8.toString()) + "=" +
                            UriUtils.encode(s, StandardCharsets.UTF_8.toString())))
            .collect(Collectors.joining("&"));
Robin Mathur
  • 447
  • 5
  • 4
1

If you need just the query string (not the whole URL) and you are using Spring Framework, you can do this:

import org.springframework.web.util.UriComponentsBuilder;

...

final String queryString = UriComponentsBuilder.newInstance()
    .queryParam("many", "7", "14", "21")
    .queryParam("single", "XYZ")
    .build()
    .toUri()
    .getQuery();

System.out.println(queryString);

the result is:

many=7&many=14&many=21&single=XYZ
malloc4k
  • 1,742
  • 3
  • 22
  • 22
-1

Personally, I'd go for a solution like this, it's incredibly similar to the solution provided by @rzwitserloot, only subtle differences.

This solution is small, simple & clean, it requires very little in terms of dependencies, all of which are a part of the Java Util package.

Map<String, String> map = new HashMap<>();

map.put("param1", "12");
map.put("param2", "cat");

String output = "someUrl?";
output += map.entrySet()
    .stream()
    .map(x -> x.getKey() + "=" + x.getValue() + "&")
    .collect(Collectors.joining("&"));

System.out.println(output.substring(0, output.length() -1));
JO3-W3B-D3V
  • 2,124
  • 11
  • 30
-1

Kotlin

mapOf(
  "param1" to 12,
  "param2" to "cat"
).map { "${it.key}=${it.value}" }
  .joinToString("&")
Hun
  • 3,652
  • 35
  • 72
-1

a very lightweight answer it works for me

public static String queryStr(Map<String, String> data) throws UnsupportedEncodingException {
    StringBuilder query = new StringBuilder();
    for (Entry<String, String> entry : data.entrySet()) {
        if (query.length() > 0) {
          query.append('&');
        }
        query.append(entry.getKey()).append('=');
        query.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
    }

    return query.toString();
}
naab
  • 1,122
  • 1
  • 8
  • 25
said
  • 19
  • 5