I have a requirement to parse a text file and generate JSON document. The text file has a pattern of text which contains a key which is a name and the value is a huge text of TSV with headers.
I could parse the text file and generate bean classes using the headers and now i want to set the data to this generated bean class. I am using reflection to do this.
Class<?> beanClass = BeanClassGenerator.beanGenerator(k, mapForBeanGeneration);
try {
Object beanClassObject = beanClass.newInstance();
lines.forEach(line -> {
if (line != null && !line.isEmpty() && !line.equals("null")) {
String[] lineData = line.split("\t");
System.out.println("LineData length :: " + lineData.length);
Method[] methods = beanClass.getMethods();
System.out.println("Methods length :: " + methods.length);
int index = 0;
for (Method m : methods) {
m.setAccessible(true);
if (m.getName().startsWith("set")) {
try {
if ((lineData.length <= index) && lineData[index] != null) {
m.invoke(beanClassObject, lineData[index]);
index++;
} else {
m.invoke(beanClassObject, " ");
}
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
});
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(beanClassObject));
} catch (InstantiationException | IllegalAccessException | JsonProcessingException e) {
e.printStackTrace();
}});
The problem with the approach is that most of the times all the column values may not have data it can be nulled.
I am wondering if there is an easier way of doing this. Any help is appreciated.
Here is the bean generation method.
public static Class<?> beanGenerator(final String className, final Map<String, Class<?>> properties) {
BeanGenerator beanGenerator = new BeanGenerator();
beanGenerator.setNamingPolicy(new NamingPolicy() {
#Override
public String getClassName(String prefix, String source, Object key, Predicate names) {
return className;
}
});
BeanGenerator.addProperties(beanGenerator, properties);
return (Class<?>) beanGenerator.createClass();
}
Here is the sample text file which needs to be converted to the JSON output.
<Data1>
Col1 col2 col3 col4 col5
even sense met has
root greatest spin mostly
gentle held introduced palace
cold equator remember grandmother
slightly butter depth like
distant second coast everyone
<Data2>
Col1 col2 col3 col4 col5 col6 col7 col8
greatest rope operation flies brown continent combination read
slightly diagram he grandfather where party fifty pour
well put plastic anyway refer careful correct furniture
how since army tongue birthday been clock official
table command specific distant cutting hill movie experience
national though stopped youth army underline five know
<Data3>
Col1 col2 col3 col4 col5 col6 col7 col8 col9 col9 col10
vessels characteristic ship joy than tomorrow high seven future trade
try gray fourth advice week stream motion musical whom tin
limited daughter large rice came home chicken wheat engine box
easy city pair strange stage visitor coach announced allow simple
jet therefore single during construction flag bigger muscle complex pleasure
income several coat range dull cattle damage jump present shake
JSON output:
[{
"<Data1>": [{
"col1": "",
"col2": "",
"col3": "",
"col4": ""
},
{
"col1": "",
"col2": "",
"col3": "",
"col4": ""
},
{
"col1": "",
"col2": "",
"col3": "",
"col4": ""
}
]
}, {
"<Data2>": [{
"col1": "",
"col2": "",
"col3": "",
"col4": "",
"col5": "",
"col6": "",
"col7": "",
"col8": ""
},
{
"col1": "",
"col2": "",
"col3": "",
"col4": "",
"col5": "",
"col6": "",
"col7": "",
"col8": ""
},
{
"col1": "",
"col2": "",
"col3": "",
"col4": "",
"col5": "",
"col6": "",
"col7": "",
"col8": ""
}
]
}]
I came up with a solution using the Maps.
Map<String, List<Map<String, String>>> finalMap = new HashMap<>();
metadataMap.forEach((k, v) -> {
List<Map<String, String>> datamap = new ArrayList<>();
String key = k;
String[] fields = v.getFields();
List<String> lines = v.getLines();
lines.forEach(line -> {
if (line != null && !line.isEmpty() && !line.equals("null")) {
String[] fieldData = line.split("\t");
Map<String, String> eachLineMap = new HashMap<>();
for (int index = 0; index < fields.length; index++) {
if (index < fieldData.length && (fieldData[index] != null && !fieldData[index].isEmpty())) {
eachLineMap.put(fields[index], fieldData[index]);
} else {
eachLineMap.put(fields[index], " ");
}
datamap.add(eachLineMap);
}
}
});
finalMap.put(key, datamap);
});
try {
output = new ObjectMapper().writeValueAsString(finalMap);
}catch(JsonProcessingException e){
e.printStackTrace();
}
You are going way overboard with your solution.
Your data is organized as an array of variable length arrays;
and does not require some crazy on-the-fly class generation solution.
As a side note,
on-the-fly class generation is not inherently crazy;
it is crazy to use on-the-fly class generation in this situation.
Do this:
Look at your data;
it is organized as follows:
first: outer key
second: exactly one line containing a variable number of space separated array of inner keys.
third: some number of lines containing values.
Design a solution to fix your problem
Read the outer key.
Use that value to create the outer key portion of your JSON.
Read the inner keys.
Store these in an array;
use LinkedList,
not ClownList (ArrayList).
Do this until the next empty line:
Read a line of values.
Write the inner JSON; use the inner keys as the keys for this.
Skip empty lines until one of the following:
If at end of file, write the ending portion of the JSON.
If you read the next outer key, goto to line 2 (Read the inner keys) above.
Write the code.
You don't need to write all that logic, you can just use Apache Commons BeanUtils; which provides a utility method (among MANY other utilities), that takes a Map of field names versus field values and populate a given bean with it:
BeanUtils.populate(target, fieldNameValueMap);
Then the only thing you need to implement is the logic to create the fieldNameValueMap Map; which you can do with this simple method:
Map<String, String> createFieldNameValueMap(String headerLine, String valuesLine) {
String[] fieldNames = headerLine.split("\t");
String[] fieldValues = valuesLine.split("\t");
return IntStream.range(0, fieldNames.length)
.mapToObj(Integer::new)
.collect(Collectors.toMap(idx -> fieldNames[idx], idx -> fieldValues[idx]));
}
You can test this solution with the following working demo:
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.beanutils.BeanUtils;
import lombok.Data;
public class DynamicBeanUtils {
static Map<String, String> createFieldNameValueMap(String headerLine, String valuesLine) {
String[] fieldNames = headerLine.split("\t");
String[] fieldValues = valuesLine.split("\t");
return IntStream.range(0, fieldNames.length)
.mapToObj(Integer::new)
.collect(Collectors.toMap(idx -> fieldNames[idx], idx -> fieldValues[idx]));
}
public static void main(String[] args) {
String headerLine = "booleanValue\tintValue\tstringValue\tdoubleValue\totherValue";
String valuesLine = "true\t12\tthis bean will be populated\t22.44\ttest string!!!";
Object target = new MyBean();
try {
BeanUtils.populate(target, createFieldNameValueMap(headerLine, valuesLine));
} catch (IllegalAccessException | InvocationTargetException e) {
// HANDLE EXCEPTIONS!
}
System.out.println(target);
}
#Data
public static class MyBean {
private String stringValue;
private double doubleValue;
private int intValue;
private boolean booleanValue;
private String otherValue;
}
}
This is the maven repository page for this dependency, so you can include it in your build: https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils/1.9.3
I used Lombok in this solution as well, only to save me the pain of writing getter/setters/toString to test this solution; but it is not required for your solution.
Complete code on GitHub
Hope this helps.
I realized that instead of creating the POJOs with a complex approach. It is better to use the Maps and convert them to JSON using Jackson ObjectMapper. Posting for others who think this might be a useful approach.
public String convert(Map<String, ? extends Metadata> metadataMap) {
String output = "";
Map<String, List<Map<String, String>>> finalMap = new HashMap<>();
metadataMap.forEach((k, v) -> {
List<Map<String, String>> datamap = new LinkedList<>();
String key = k;
String[] fields = v.getFields();
List<String> lines = v.getLines();
lines.forEach(line -> {
if (line != null && !line.isEmpty() && !line.equals("null")) {
String[] fieldData = line.split("\t",-1);
Map<String, String> eachLineMap = new HashMap<>();
for (int index = 0; index < fields.length; index++) {
if (index < fieldData.length && (fieldData[index] != null && !fieldData[index].isEmpty())) {
eachLineMap.put(fields[index], fieldData[index]);
} else {
eachLineMap.put(fields[index], " ");
}
datamap.add(eachLineMap);
}
}
});
finalMap.put(key, datamap);
});
try {
output = new ObjectMapper().writeValueAsString(finalMap);
}catch(JsonProcessingException e){
e.printStackTrace();
}
return output;
}
Related
I have mobile numbers in database table column, in a format of country_code followed by mobile_number
So Mobile Number format is like this,
+91123456789 // country code of India is +91 followed by mobile number
+97188888888 // Country code of UAE +971
I have one HashMap containing CountryCodes of 5 countries like this,
map.put("+91","India")
map.put("+94","Sri Lanka")
map.put("+881","Bangladesh")
map.put("+971","UAE")
map.put("+977","Nepal")
My Bean Structure is something like this
class UserDetails {
// other fields
String countryCode;
String mobileNumber;
}
Now my task is to take the mobile number from Database table column and split it in two parts and set countryCode and mobileNumber, but country code length(in map's key) varies between 3 and 4. This checking can be done by using subString() and equals() but I don't think it's correct way, So what would be the elegant(may be checking in map key) way to solve this issue?
Although there is a library which seems to already do the trick, I think I'd go for an easy self-written solution:
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class CountryExtractor {
private static final Map<String, String> COUNTRY_MAPPING = new HashMap<>();
static {
COUNTRY_MAPPING.put("+91", "India");
COUNTRY_MAPPING.put("+94", "Sri Lanka");
COUNTRY_MAPPING.put("+881", "Bangladesh");
COUNTRY_MAPPING.put("+971", "UAE");
COUNTRY_MAPPING.put("+977", "Nepal");
}
public static void main(String[] args) {
String[] inputs = new String[] { "+91123456789", "+97188888888" };
for (String input : inputs) {
System.out.println(Arrays.toString(parseNumber(input)));
}
}
private static String[] parseNumber(String number) {
for (String countryCode : COUNTRY_MAPPING.keySet()) {
if (number.startsWith(countryCode)) {
return new String[] { countryCode, number.replace(countryCode, "") };
}
}
return new String[0];
}
}
Output:
[+91, 123456789]
[+971, 88888888]
Note that this may not work correctly when a mobile prefix is a substring of another, but according to Wikipedia country calling codes are prefix codes and therefore guarantee that "there is no whole code word in the system that is a prefix (initial segment) of any other code word in the system".
IMHO a single map is better. An example;
public static void main(String args[]) throws Exception {
Map<String, String> map = new HashMap<>();
put(map, "+91", "India");
put(map, "+94", "Sri Lanka");
put(map, "+881", "Bangladesh");
put(map, "+971", "UAE");
put(map, "+977", "Nepal");
map = Collections.unmodifiableMap(map);
String mobileNumber = "+91123456789";
System.out.println(countryCode(map.keySet(), mobileNumber));
}
private static void put(Map<String, String> map, String key, String value) {
if (countryCode(map.keySet(), key) != null) {
throw new IllegalArgumentException("...");
}
map.put(key, value);
}
public static String countryCode(Set<String> countryCodes, String number) {
if (number == null || number.length() < 3) {
throw new IllegalArgumentException("...");
}
String code = number.substring(0, 3);
if (!countryCodes.contains(code)) {
if (number.length() > 3) {
code = number.substring(0, 4);
if (!countryCodes.contains(code)) {
code = null;
}
} else {
code = null;
}
}
return code;
}
You could use two maps for country code of different lengths and then search first for a match with 3 letters, and then with 4 letters.
HashMap<String, String > threeLetterCodes = new HashMap<String, String>();
threeLetterCodes.put("+91","India");
threeLetterCodes.put("+94","Sri Lanka");
HashMap<String, String > fourLetterCodes = new HashMap<String, String>();
fourLetterCodes.put("+881","Bangladesh");
fourLetterCodes.put("+971","UAE");
fourLetterCodes.put("+977","Nepal");
String test = "+97188888888";
String prefix = test.substring(0, 3);
String country = threeLetterCodes.get(prefix);
if (country == null) {
prefix = test.substring(0, 4);
country = fourLetterCodes.get(prefix);
}
System.out.println(country);
Output:
UAE
I have a ArrayList of String array. String array looks something like below
["Type-A","Date","Expert"]
["Type-A","Date","07 Expert"]
["Type-A","Date","10 Expert"]
["Type-B","Date","Expert"]
["Type-B","Date","10 Expert"]
["Type-C","Date","07 Expert"]
["Type-C","Date","10 Expert"]
Consider arraylist have above string arrays. I want to take out unique type from each type from arraylist on thee basis of expertise level. If we cosider above list then I want Type-A with level as Expert and Type-B with level as Expert and Type-C with level as Expert10 as in hierarchy Expert is at top and Expert10 and Expert07 are lower to it.
The structure is like I have arraylist which contains string array. Each String array record has Type and expertise level. Arraylist can have multiple records of same Type with different expertise level. I want record of each Type but with highest expertise level. I have a list if different expertise level. Now my confusion is how to use that expertise level list to take out or make another arraylist with single record of each type with highest expertise level.
List of expertise level.
Expert
10 Expert
07 Expert
Professional
Systems
10 System
07 System
Just loop it and find out, using a map to store current max expert item for each type during looping:
private boolean isMoreExpert(String expertPicked, String expertToCompare){
//TODO: return true is expertPicked is lower expert than expertToCompare, false otherwise
return false;
}
private List<String[]> mapToList<Map<String[]> map>{
//TODO: iterate the map and store items to a list
return null;
}
private List<String[]> getMostExpertListOfEachType(List<String[]> items){
Map<String, String[]> tempRecord = new HashMap<>();
for(String[] item in items){
//assume first item is the type
String[] current = tempRecord.get(item[0]);
//assume the third item is the expert
if(current != null ){
if(isMoreExpert(current[2], item[2])){tempRecord.put(item[0], item);}
}else{
tempRecord.put(item[0], item);
}
}
return mapToList(tempRecord);
}
Have not tested the code yet:)
Here I used Pattern-Matcher to extract expert level. Iterate the given array and then created array, If Same type is found - compare expert level - if expert level if created is smaller - then replace with iterated expert.
List<String[]> givenExp = Arrays.asList(
new String[]{"Type-A", "Date", "Expert"},
new String[]{"Type-A", "Date", "Expert07"},
new String[]{"Type-A", "Date", "Expert10"},
new String[]{"Type-B", "Date", "Expert"},
new String[]{"Type-B", "Date", "Expert10"},
new String[]{"Type-C", "Date", "Expert07"},
new String[]{"Type-C", "Date", "Expert10"});
List<String[]> filteredExp = new ArrayList<>();
Pattern pat = Pattern.compile("(?<=Expert)\\d*");
Matcher gmat, fmat;
String gexplvl, fexplvl;
int giexplvl, fiexplvl;
main:
for (String[] gexp : givenExp) {
for (String[] fexp : filteredExp) {
if (fexp[0].equals(gexp[0])) {
gmat = pat.matcher(gexp[2]);
fmat = pat.matcher(fexp[2]);
gmat.find();
fmat.find();
gexplvl = gmat.group();
fexplvl = fmat.group();
if (gexplvl.length() == 0) {
filteredExp.remove(fexp);
filteredExp.add(gexp);
} else {
if (fexplvl.length() != 0 && Integer.parseInt(fexplvl) < Integer.parseInt(gexplvl)) {
filteredExp.remove(fexp);
filteredExp.add(gexp);
}
}
continue main;
}
}
filteredExp.add(gexp);
}
for (String[] fexp : filteredExp) {
for (String val : fexp) {
System.out.printf("%-10s", val);
}
System.out.println();
}
Output :
Type-A Date Expert
Type-B Date Expert
Type-C Date Expert10
With Java 8 Streams, it is as simple as:
list.stream().sorted((s1, s2) -> s2[2].compareTo(s1[2])).filter(isMaxExpert())
.collect(Collectors.toList());
private static Predicate<String[]> isMaxExpert() {
final Map<String, String[]> map = new HashMap<>();
return p -> {
if (map.get(p[0]) != null) {
return false;
} else {
map.put(p[0], p);
return true;
}
};
};
Usage:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SelectOne {
public static void main(String[] args) {
List<String[]> list = new ArrayList<>();
list.add(new String[] { "Type-A", "Date", "Expert" });
list.add(new String[] { "Type-A", "Date", "07 Expert" });
list.add(new String[] { "Type-A", "Date", "10 Expert" });
list.add(new String[] { "Type-B", "Date", "Expert" });
list.add(new String[] { "Type-B", "Date", "10 Expert" });
list.add(new String[] { "Type-C", "Date", "07 Expert" });
list.add(new String[] { "Type-C", "Date", "10 Expert" });
List<String[]> modified = list.stream()
.sorted((s1, s2) -> s2[2].compareTo(s1[2])).filter(isMaxExpert())
.collect(Collectors.toList());
for (String[] strArray : modified) {
System.out.println(" " + strArray[0] + " " + strArray[1] + " "
+ strArray[2]);
}
}
private static Predicate<String[]> isMaxExpert() {
final Map<String, String[]> map = new HashMap<>();
return p -> {
if (map.get(p[0]) != null) {
return false;
} else {
map.put(p[0], p);
return true;
}
};
};
}
I recently started working on an app that does a request to a server and gets a json response.
The "thing" functioned beautifully until i had to implement new stuff in the list and now i have a hard time to fix it.
Any help is very appreciated:
class RemoteConfig
{
// names and type must match what we get from the remote
String[] username;
ArrayList<accDetails> in_groups;
String[] in_groups_sorted;
class accDetails
{
int group_id;
String group_label;
Boolean _is_system;
}
This is just a part of how the class starts, and here is how the json reponse looks like:
{
"username":[
"mike"
],
"in_groups":[
{
"group_id":2,
"group_label":"All users",
"_is_system":true
},
{
"group_id":4372,
"group_label":"Privileged User",
"_is_system":false
},
{
"group_id":4979,
"group_label":"Supervisor",
"_is_system":false
}
]
}
The problem that i encounter now, is that i have no idea on how to split the in_groups array list and get into String[] in_groups_sorted the value of Group_label if the _is_system value is false.
Any help is highly appreciated.
Thank you,
Mike
After checking the responses, the cleanest and simplest was the one provided by Abbe:
public String[] groupSettings()
{
String[] levels = new String[] {};
if (remoteConfig != null && remoteConfig.in_groups != null){
for (accDetails ad: remoteConfig.in_groups)
{
if (!ad._is_system) {
levels = ArrayUtils.addAll(levels, ad.group_label); ;
}
}
}
return levels;
}
From your question, I suppose the JSON is already parsed and stored in the in_groups field of RemoteConfig class. And you just need to filter the information you need to populate the in_group_sorted field.
Add the following to the RemoteConfig class:
public initGroupSorted() {
// Temporary list, since we don't know the size once filtered
List<String> labels = new ArrayList<>();
for (accDetails ad : in_groups) {
if (ad._is_system) {
groups.add(ad.group_label);
}
}
in_group_sorted = labels.toArray(new String[labels.size()]);
}
if you donĀ“t want to change the way you parse your JSON, you could always do this:
Let accDetails implement Comparable and then use Collections.sort passing in_groups.
if you really want the String[] you could always iterate over in_groups, add to in_groups_sorted and then using Arrays.sort
Mike, let me give you something that should get you going. From your question i got the feeling that your problem was on how to parse the JSON, so before you go write your own parser, consider the following piece of code that i just wrote:
public void createObjects(String rawJSON) {
try {
JSONObject object = new JSONObject(rawJSON);
JSONArray username = object.getJSONArray("username");
JSONArray inGroups = object.getJSONArray("in_groups");
RemoteConfig config = new RemoteConfig();
config.in_groups = new ArrayList<>();
config.username = username.getString(0);
for (int i = 0; i < inGroups.length(); i++) {
JSONObject group = inGroups.getJSONObject(i);
if (!group.getBoolean("_is_system")) {
accDetails details = new accDetails();
details.group_id = group.getInt("group_id");
details.group_label = group.getString("group_label");
details._is_system = false;
config.in_groups.add(details);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
Here is a Java 8 Solution using Stream's filter,sorted, and map methods:
//ArrayList<accDetails> in_groups is already populated
Stream<accDetails> tempStream= in_groups.stream().filter(p -> p._is_system == false);
tempStream= tempStream.sorted((accDetails o1, accDetails o2) -> o1.group_label.compareTo(o2.group_label));
String[] in_groups_sorted = tempStream.map(s -> s.group_label).toArray(String[]::new);
Separated the calls for visibility, but they can be a one liner:
String[] in_groups_sorted = in_groups.stream().filter(p -> p._is_system == false).sorted((accDetails o1, accDetails o2) -> o1.group_label.compareTo(o2.group_label)).map(s -> s.group_label).toArray(String[]::new);
In a JSON file, each object inside the file is composed by different type of JSON elements. (integer, string, array, array of objects, etc.)
My target is to list all element name and corresponding type. May I know how can I do that in Gson? The purpose of this is for creating a Hive schema.
Example:
{
"number": 1,
"ts": "1386848002",
"cmpg": [
{
"id": 476,
"mcp": 0,
"deals": [ ],
"cookie": "uid:123",
"bid": [
{
"bId": 0,
"status": "ZB",
"rmtchID": -1
}
]
}
]
}
Output:
number int,
ts String,
cmpg array<map<String, Object>> // not sure how to interpret this...
I wrote this simple class that shows you how use some Gson classes to get what you need.
package stackoverflow.questions.q19124387;
import java.util.Map;
import com.google.gson.*;
public class Q20624042 {
private static String printClass(JsonElement je, String ident) {
StringBuilder sb = null;
if (je.isJsonNull())
return "null";
if (je.isJsonPrimitive()) {
if (je.getAsJsonPrimitive().isBoolean())
return "Boolean";
if (je.getAsJsonPrimitive().isString())
return "String";
if (je.getAsJsonPrimitive().isNumber()){
return "Number";
}
}
if (je.isJsonArray()) {
sb = new StringBuilder("array<");
for (JsonElement e : je.getAsJsonArray()) {
sb.append(printClass(e, ident+ " "));
}
sb.append(">");
return sb.toString();
}
if (je.isJsonObject()) {
sb = new StringBuilder("map<\n");
for (Map.Entry<String, JsonElement> e : je.getAsJsonObject().entrySet()) {
sb.append(ident);
sb.append(e.getKey()).append(":");
sb.append(printClass(e.getValue(), ident+" "));
sb.append("\n");
}
sb.append(ident);
sb.append(">");
return sb.toString();
}
return "";
}
public static void main(String[] args) {
String json = "{" + "\"number\":1," + "\"ts\":\"1386848002\"," + "\"cmpg\":[{\"id\":476,\"mcp\":0.0000,\"deals\":[],\"cookie\":\"uid:123\",\"bid\":[{\"bId\":0,\"status\":\"ZB\",\"rmtchID\":-1}]}]}";
JsonElement je = new JsonParser().parse(json);
System.out.println(printClass(je," "));
}
}
And this is the result with your JSON string:
map<
number:Number
ts:String
cmpg:array<map<
id:Number
mcp:Number
deals:array<>
cookie:String
bid:array<map<
bId:Number
status:String
rmtchID:Number
>>
>>
>
JSON has a recursive nature, so the only way to approach to this kind of problem is to write a recursive method. My indentation system is quite naive, I put indentation only to show the correspondence to your JSON, maybe you do not even need that. Keep in mind that in JSON you do not have difference between integer and doubles,
JSON can represent four primitive types (strings, numbers,
booleans, and null) and two structured types (objects and arrays)
so if you what that distinction you have change a bit my method.
.
Does anyone have, or know of, a java class that I can use to manipulate query strings?
Essentially I'd like a class that I can simply give a query string to and then delete, add and modify query string KVP's.
Thanks in advance.
EDIT
In response to a comment made to this question, the query string will look something like this;
N=123+456+112&Ntt=koala&D=abc
So I'd like to pass this class the query string and say something like;
String[] N = queryStringClass.getParameter("N");
and then maybe
queryStringClass.setParameter("N", N);
and maybe queryStringClass.removeParameter("N");
Or something to that effect.
SOmething like this
public static Map<String, String> getQueryMap(String query)
{
String[] params = query.split("&");
Map<String, String> map = new HashMap<String, String>();
for (String param : params)
{
String name = param.split("=")[0];
String value = param.split("=")[1];
map.put(name, value);
}
return map;
}
To iterate the map simply:
String query = url.getQuery();
Map<String, String> map = getQueryMap(query);
Set<String> keys = map.keySet();
for (String key : keys)
{
System.out.println("Name=" + key);
System.out.println("Value=" + map.get(key));
}
You can also use Google Guava's Splitter.
String queryString = "variableA=89&variableB=100";
Map<String,String> queryParameters = Splitter
.on("&")
.withKeyValueSeparator("=")
.split(queryString);
System.out.println(queryParameters.get("variableA"));
prints out
89
This I think is a very readable alternative to parsing it yourself.
Edit: As #raulk pointed out, this solution does not account for escaped characters. However, this may not be an issue because before you URL-Decode, the query string is guaranteed to not have any escaped characters that conflict with '=' and '&'. You can use this to your advantage in the following way.
Say that you must decode the following query string:
a=%26%23%25!)%23(%40!&b=%23%24(%40)%24%40%40))%24%23%5E*%26
which is URL encoded, then you are guaranteed that the '&' and '=' are specifically used for separating pairs and key from value, respectively, at which point you can use the Guava splitter to get:
a = %26%23%25!)%23(%40!
b = %23%24(%40)%24%40%40))%24%23%5E*%26
Once you have obtained the key-value pairs, then you can URL decode them separately.
a = &#%!)#(#!
b = #$(#)$##))$#^*&
That should cover all cases.
If you are using J2EE, you can use ServletRequest.getParameterValues().
Otherwise, I don't think Java has any common classes for query string handling. Writing your own shouldn't be too hard, though there are certain tricky edge cases, such as realizing that technically the same key may appear more than once in the query string.
One implementation might look like:
import java.util.*;
import java.net.URLEncoder;
import java.net.URLDecoder;
public class QueryParams {
private static class KVP {
final String key;
final String value;
KVP (String key, String value) {
this.key = key;
this.value = value;
}
}
List<KVP> query = new ArrayList<KVP>();
public QueryParams(String queryString) {
parse(queryString);
}
public QueryParams() {
}
public void addParam(String key, String value) {
if (key == null || value == null)
throw new NullPointerException("null parameter key or value");
query.add(new KVP(key, value));
}
private void parse(String queryString) {
for (String pair : queryString.split("&")) {
int eq = pair.indexOf("=");
if (eq < 0) {
// key with no value
addParam(URLDecoder.decode(pair), "");
} else {
// key=value
String key = URLDecoder.decode(pair.substring(0, eq));
String value = URLDecoder.decode(pair.substring(eq + 1));
query.add(new KVP(key, value));
}
}
}
public String toQueryString() {
StringBuilder sb = new StringBuilder();
for (KVP kvp : query) {
if (sb.length() > 0) {
sb.append('&');
}
sb.append(URLEncoder.encode(kvp.key));
if (!kvp.value.equals("")) {
sb.append('=');
sb.append(URLEncoder.encode(kvp.value));
}
}
return sb.toString();
}
public String getParameter(String key) {
for (KVP kvp : query) {
if (kvp.key.equals(key)) {
return kvp.value;
}
}
return null;
}
public List<String> getParameterValues(String key) {
List<String> list = new LinkedList<String>();
for (KVP kvp : query) {
if (kvp.key.equals(key)) {
list.add(kvp.value);
}
}
return list;
}
public static void main(String[] args) {
QueryParams qp = new QueryParams("k1=v1&k2&k3=v3&k1=v4&k1&k5=hello+%22world");
System.out.println("getParameter:");
String[] keys = new String[] { "k1", "k2", "k3", "k5" };
for (String key : keys) {
System.out.println(key + ": " + qp.getParameter(key));
}
System.out.println("getParameters(k1): " + qp.getParameterValues("k1"));
}
}
Another way is to use apache http-components. It's a bit hacky, but at least you leverage all the parsing corner cases:
List<NameValuePair> params =
URLEncodedUtils.parse("http://example.com/?" + queryString, Charset.forName("UTF-8"));
That'll give you a List of NameValuePair objects that should be easy to work with.
You can create a util method and use regular expression to parse it. A pattern like "[;&]" should suffice.