Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to concatenate all map key-values into a string, using streams, with replacements made on each value?

I'm a Java 7 person, and have been commanded to never use a for-loop again, with only partial tongue in cheek. I need to take a Map<String, String>, and end up with a string of command-line parameters, as in the output of the following code (ignoring the "streamy" line for the moment):

forLoop: --name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name1 "value1"
resetAllValues: --name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name1 "value1"
streamy: --name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name1 "value1"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name5 "value5"--name5 "value5"--name4 "value4"--name5 "value5"--name5 "value5"--name4 "value4"--name3 "value3"--name2 "{\"x\": \"y\"}"--name1 "value1"

The names/keys remain unchanged. The values need to be placed in double quotes, and all existing double quotes need to be escaped (\"). The close-quotes should be followed by a space.

I've done it the pre-java-8 way in forLoop, and partially with streams in resetAllValues, which actually alters each entry. The streamy one is way wrong, as it's repeatedly appending all current output onto the next element...

How can this be done efficiently with streams? And how can it be done in a way that doesn't alter the map entries or use a builder? I'm not seeing it yet.

package working;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class Temp {
   public static void main(String[] cmd_lineParams) {
      Map<String, String> map = new HashMap<>(5);
      map.put("name1", "value1");
      map.put("name2", "{\"x\": \"y\"}");
      map.put("name3", "value3");
      map.put("name4", "value4");
      map.put("name5", "value5");
      forLoop(map);
      resetAllValues(new HashMap<String, String>(map));
      streamy(new HashMap<String, String>(map));
   }
   private static final Matcher matcher = Pattern.compile("\"").matcher("ignored input");
   private static final void forLoop(Map<String, String> map) {
      StringBuilder builder = new StringBuilder();
      for(Map.Entry<String, String> entry : map.entrySet()) {
         String value = matcher.reset(entry.getValue()).replaceAll("\\\\\"");
         builder.append("--").append(entry.getKey()).append(" \"").append(value).append("\"");
      }
      System.out.println("forLoop: " + builder.toString());
   }

Continued...

   private static final void resetAllValues(Map<String, String> map) {
      map = map.entrySet().stream()
         .collect(Collectors.toMap(entry -> entry.getKey(),
                                   entry -> matcher.reset(entry.getValue()).replaceAll("\\\\\\\"")));
      StringBuilder builder = new StringBuilder();
      for(Map.Entry<String, String> entry : map.entrySet()) {
         builder.append("--").append(entry.getKey()).append(" \"").append(entry.getValue()).append("\"");
      }
      System.out.println("resetAllValues: " + builder.toString());
   }
   private static final void streamy(Map<String, String> map) {
      StringBuilder builder = new StringBuilder();
      map.forEach((k,v) -> builder.append(
         builder.append("--").append(k).append(" \"").append(
            matcher.reset(v).replaceAll("\\\\\"")).append("\"")));
      System.out.println("streamy: " + builder.toString());
   }
}

(My nine-year-old says I need to say "difficulty" somewhere in this question. So: difficulty.)

like image 383
aliteralmind Avatar asked Jul 19 '15 01:07

aliteralmind


People also ask

How do you add a string value to a string Map?

Maps are associative containers that store elements in a specific order. It stores elements in a combination of key values and mapped values. To insert the data in the map insert() function in the map is used.


2 Answers

I guess since you said difficulty, I'll have to answer this! :)

map.entrySet().stream().map((entry) -> //stream each entry, map it to string value
            "--" + entry.getKey() + " \"" + entry.getValue().replaceAll("\"", "\\\\\"") + "\"")
            .collect(Collectors.joining(" ")); //and 'collect' or put them together by joining

I personally don't like using streams because it gets ugly pretty quick, but its useful for simpler loops. (join all values with some character for example) With this however you can easily parallelize it by using parallelStream() instead of stream()

If you wanted the values in some sort of order so its not so random (as it will be with HashMap) you can do a sort before map:

.stream().sort((e1, e2) -> e1.getValue().compareTo(e2.getValue()))
.map(...)...

Just remember that those are Map.Entry objects.

UPDATE: Tagir Valeev's answer below is better, as it shows the best practice instead of just making it work. It also remedies the initial gripe I had with streams + lambdas couple years back when I wrote this answer (getting too ugly).

like image 157
Aarjav Avatar answered Oct 15 '22 15:10

Aarjav


Always try to decompose your complex problem into simple and independent parts. Here (both for stream and non-stream solution) it's better to put the escaping code into the separate method:

static String quoteValue(String value) {
    return "\"" + value.replaceAll("\"", "\\\\\"") + "\"";
}

This way you have a separate piece of code with clear semantic which can be reused and tested separately. For example, in future you may need to escape a back-slash symbol as well, because currently you may have problems decoding the string which originally had the mix of back-slashes and quotes. If would be much easier to debug this problem if you have a separate quoteValue method (this way you should not create the test map, just test string).

After that the stream solution becomes less confusing:

map.entrySet().stream().map(entry ->
        "--" + entry.getKey() + " "+ quoteValue(entry.getValue()))
        .collect(joining(" "));

You can go further and add one more method to format the whole entry as well:

static String formatEntry(String key, String value) {
    return "--" + key + " " + quoteValue(value);
}

map.entrySet().stream().map(e -> formatEntry(e.getKey(), e.getValue()))
                       .collect(joining(" "));
like image 20
Tagir Valeev Avatar answered Oct 15 '22 15:10

Tagir Valeev