use reflection to set or get array in a Java Map - java

Is there a way to get or set an array element stored in a Java Map?
Example:
If we have a map like this:
{
name: "Blah",
friends: ["Foo", "Bar"]
}
Map<String, Object> myMap = new HashMap<>();
List<String> friends = new ArrayList<>();
myMap.put("name", "Blah");
myMap.put("friends", friends);
Is it possible to use Reflection to get or set the first element in the friends array in the "myMap" from the string: "myMap.friends[0]"

Your question is not very clearly written and I believe that's why you are not getting the answer you expect but, If I understood your question correctly, you need to parse the following input string at runtime that you don't know beforehand:
myMap.friends[0]
And this should be parsed into components like:
mapName = "myMap"
mapKey = "friends"
valueIndex = 0
And with this information, you need to manipulate data in a Map at runtime through reflection.
Note: This only makes sense if you could potentially have more complex expressions, using different sort of objects and accessing nested properties of retrieved objects, otherwise you wouldn't need reflection at all.
Note 2: You may want to have a look at JXPath which already does a lot of this for you based on a XPath-like syntax for navigating object graphs.
That said, if my assumptions are correct and you still want to do it yourself, consider the following example.
For the sake of demonstration, let's consider our map is returned by a method myMap inside a Context.
private static class Context {
public Map<String, Object> myMap() {
Map<String, Object> myMap = new HashMap<>();
List<String> friends = new ArrayList<>();
friends.add("Foo");
friends.add("Bar");
myMap.put("name", "Blah");
myMap.put("friends", friends);
return myMap;
}
}
I'm assuming you are already parsing the input string into the different components. If not, for this simple string you could do it with simple regular expressions. If you already have the components, let's consider the following method:
public static Object readContextMap(Context context,
String mapName, String mapKey, Integer mapValueIndex) throws Exception {
// gets Context class for inspection
Class<?> cls = context.getClass();
// search for a method based on supplied mapName
Method mapMethod = cls.getDeclaredMethod(mapName);
// get a value from the retrieved map based on mapKey
Object mapValue = mapMethod.getReturnType()
.getDeclaredMethod("get", Object.class)
.invoke(mapMethod.invoke(context), mapKey);
// if the result is of type list, use the index to return the indexed element
if (List.class.isAssignableFrom(mapValue.getClass())) {
return ((List<?>)mapValue).get(mapValueIndex);
}
// otherwise return the object itself
return mapValue;
}
For testing purposes, consider the following main method:
public static void main(String[] args) throws Exception {
Context context = new Context();
String input = "myMap.friends[0]";
// parse input into...
String mapName = "myMap";
String mapKey = "friends";
Integer valueIndex = 0;
Object firstFriend = readContextMap(context, mapName, mapKey, valueIndex);
System.out.println(firstFriend);
// prints Foo
Object name = readContextMap(context, "myMap", "name", null);
System.out.println(name);
// prints Blah
}
This should be approximately what you want. You can easily create variations of this to set values as well. Please bear in mind that this code is just for demo purposes and needs a better error handling (e.g. verify if the context is really returning a map and nothing else).
This should be something along the lines you are looking for.

There's no need to use reflection here. You can simply cast it (which is also unsafe, but less so).
You can just do this:
List<String> friends = (List<String>) myMap.get("friends");
friends.set(0, "Bob");

Related

What is the reasoning on modelling a class to represent JSON data and do I need to?

I have come across this question on StackOverflow which asks about converting JSON to Java. The answer shows that another class is modelled to represent the JSON data as well as an object being created and I don't understand why.
Does that object now contain all the information after Gson reads the content or only one key/value pair? If it only contains 1 key/value pair, I'm assuming I would need to create multiple objects for the JSON that I have below which I can the use a loop to iterate over and add the values to a drop down menu?
{
"1": "Annie",
"2": "Olaf",
"3": "Galio",
"4": "TwistedFate",
"5": "XinZhao",
"6": "Urgot",
"7": "Leblanc",
"8": "Vladimir",
"9": "FiddleSticks",
"10": "Kayle",
"11": "MasterYi",
"12": "Alistar",
"13": "Ryze",
"14": "Sion",
"15": "Sivir",
"16": "Soraka",
"17": "Teemo",
"18": "Tristana",
"19": "Warwick",
"20": "Nunu"
}
Essentially what I am aiming to do is:
1) Create a list of names with the Values.
2) Sort the list of names (as it comes unsorted) in alphabetical order
3) Loop through the list and add each name to a drop down menu
4) When a name in the drop down menu is selected, the key associated with that value is passed to another url which receives more data.
Sorry if this is unclear. I've spent a couple of hours trying to understand how to get elements from JSON and display it, as well as trying to create a list where I can use the key to display information the name but have had no luck except for using a for-each loop.
Let's use Jackson's feature that allows you to map any property to a single method (you don't really need a getter here I believe). Just swap the key and value in this universal setter, and add to a TreeMap, which is already sorted by key (name). Then you can output the keys (names) in the alphabetical order and get an ID by name easily.
public static void main(String[] args) throws IOException {
String json = "....."; // your JSON string here
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
ReverseMap pairs = mapper.readValue(json, ReverseMap.class);
for (Map.Entry<Object, String> entry : pairs.getValues().entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
public class ReverseMap {
private TreeMap<Object, String> mapping = new TreeMap<>();
#com.fasterxml.jackson.annotation.JsonAnySetter
public void add(String name, Object value) {
mapping.put(value, name);
}
public Map<Object, String> getValues() {
return mapping;
}
}
Gson Bean Mapping Solution
Okay, what you have is a bit unusual for a JSON object; the keys (the numbers in your case) essentially represent properties of their contained object. That's workable, but you have to understand that, for example, when looking for "Annie" in the JSON object, if you use Gson to map to a "bean" class, which we'll call Data (as in the linked example), then you'd have to create a data object like so:
class Data {
private String _1;
// ...
private String _20;
public String get1() { return _1; }
public void set1(String _1) { this._1 = _1; }
// ...
public String get20() { return _20; }
public void set20(String _20) { this._20 = _20; }
}
And by using Data data = new Gson().fromJson(myJsonString, Data.class); on the given string, you'd be able to find "Annie" by calling... uh... data.get1()?
Clearly, this isn't a good solution.
Better Solutions
Since your data doesn't follow the typical format for a JSON object, you have two options:
If you can, refactor your JSON representation to a more verbose, but better representation for parsing.
Use a different approach to parse the existing JSON.
Solution 1: Changing the JSON representation
Refactoring the JSON would result in an object that (preferably) would look like this:
{
"champions" : [
{
"index" : 1,
"name" : "Annie"
},
{
"index" : 2,
"name" : "Olaf"
},
// ...
]
}
This could map easily to a couple of beans that look like this:
class Data {
private List<Champion> champions;
// TODO getters and setters
}
class Champion {
private int index;
private String name;
// TODO getters and setters
}
However, this adds a lot of unnecessary clutter to the JSON object, and isn't really necessary with only two fields per champion (the name, and their index).
You could simplify that further like so:
{
"champions" : [
"Annie",
"Olaf",
// ...
]
}
The bean class for that would then be:
class Data {
private List<String> champions;
// TODO getters and setters
}
Much simpler, but still requires a change to the JSON you're getting, which in some situations isn't possible. If you used this, though, you could also get rid of the "bean" class entirely, via:
List<String> champions = (List<String>) new Gson().fromJson(myJsonString, new TypeToken<List<String>>(){}.getType());
Solution 2: Changing how the JSON is parsed
The arguably better and cleaner solution is just to change how the JSON is parsed.
The goal here (if I understand you correctly) is to parse the JSON and spit out a collection of strings representing each champion's name, accessible by the numeric index of the champion in the JSON representation.
As such, and because of the way the JSON object is laid out as a simple mapping of strings to strings, we can use Gson to pipe directly into a Map<String, Object>, like so:
Map<String, String> mappedValues = new Gson().fromJson(myJsonString, Map.class);
String anniesName = mappedValues.get("1"); // "Annie"
String olafsName = mappedValues.get("2"); // "Olaf"
boolean hasTwentyOneElements = mappedValues.containsKey("21"); // false
This is shorter, requires no "bean" classes, and keeps the original JSON representation. The downside is that you can't easily tell whether the indices of each entry are correct and consistent; ie. if someone types in the wrong number, or deletes one of the entries.
To get a container of all keys, you just use mappedValues.keySet(), and to get a container of all key-value pairs, you use mappedValues.entrySet(), which gives you a Set<Map.Entry<String, String>>. Both of those can be iterated over, and may be in random order (I'm not sure whether the underlying Map implementation preserves insertion order or not).
To get the index for a given name (ie. champ), you'd use something similar to the following:
String index = null;
for (Map.Entry<String, String> entry : mappedValues.entrySet()) {
if (champ.equals(entry.getValue())) {
index = entry.getKey();
break;
}
}
Of course, you'd have to check to see if index is null after this, and handle that appropriately, but it's easily doable.
EDIT: #vempo's answer provides a cleaner, more efficient lookup strategy by means of inverting the map (although the answer is written for Jackson, instead of Gson); an adaptation of this for Gson is as follows (and yes, there is a vastly superior version in java-8, left out for sake of availability):
public Map<String, String> invertMap(Map<String, String> input) {
Map<String, String> newMap = new LinkedTreeMap<String, String>(); // TODO Pick optimal storage class
for (Map.Entry<String, String> entry : input.entrySet()) {
newMap.put(entry.getValue(), entry.getKey());
}
return newMap;
}
// ...
Map<String, String> mappedValues = invertMap(new Gson().fromJson(myJsonString, Map.class));
String annieIndex = mappedValues.get("Annie"); // "1"
String olafIndex = mappedValues.get("Olaf"); // "2"
It's worth noting that this sacrifices efficiency of constructing the map by effectively building it twice (once by Gson and once more to invert), but it makes value lookup much more efficient.

Error: The method convertProspectIds(Map<String,String>) in the type CompassADFAdaptor is not applicable for the arguments (List<String>)

First, my apologies as I am a brand new Java developer and have been tasked to work on a massive application, so I am very out of my league.
I currently have this code. Basically, it looks for some values on the URL of a web application, and then passes the name of the URL variable and the value of the URL variable into the application, where it will then be added to an output stream of text. So the variables would look like "&LeadID=123" which would translate to the "LeadId" and "123". At that point, we reformat it into an XML value, such as
<id sequence="1" source="leadId">123</id>
This is the code that is responsible for checking for the variables, and then putting them into a Map, and then (in theory) passing them to the code which will format them:
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
Map<String, String> ProspectTrackingKeys = new HashMap<String, String>();
if (recipSettings.containsKey("leadId")) {
ProspectTrackingKeys.put("LeadId", recipSettings.get("leadId"));
}
if (recipSettings.containsKey("wtk")) {
ProspectTrackingKeys.put("wtk", recipSettings.get("wtk"));
}
if (recipSettings.containsKey("efd")) {
ProspectTrackingKeys.put("efd", recipSettings.get("efd"));
}
if (recipSettings.containsKey("aid")) {
ProspectTrackingKeys.put("aid", recipSettings.get("aid"));
}
if (recipSettings.containsKey("oth")) {
ProspectTrackingKeys.put("oth", recipSettings.get("oth"));
}
list.add(ProspectTrackingKeys);
prospect.setProspect(adaptor.convertProspectIds((Map<String, String>) prospectIds));
However, the last line of code (above) is generating these errors:
The method setProspect(ArrayList) is undefined for the type Prospect
The method convertProspectIds(Map) in the type CompassADFAdaptor is not applicable for the arguments (List)
This is in reference to this code, which is where the values should be passed into, and then formatted as the XML text:
public ArrayList<ProspectId> convertProspectIds(
Map<String, String> recipientSettings) {
ArrayList<ProspectId> prospectIdList = new ArrayList<ProspectId>();
int i = 0;
Iterator it = recipientSettings.entrySet().iterator();
while(it.hasNext()) {
Map.Entry pairs = (Map.Entry)it.next();
ProspectId Id = new ProspectId();
Id.setSequence(i++);
Id.setSource(ProspectIdSource.valueOf(pairs.getKey().toString()));
Id.setValue(pairs.getValue().toString());
prospectIdList.add(Id);
}
return prospectIdList;
}
I am not quite sure what I am missing ... can someone help me out?
convertProspectIds takes Map as an argument and you are passing it a List. You need to either change the method signature or convert List to Map.
The same goes for setProspect method. It does not expect ArrayList as a parameter.

Best practice for method with Map return

Suppose i have a method which has map as return type and uses generics.
I would like to know what is the best practice of filling that Map object.
Please see the snippet.
public Map<String,?> getEmployeeInfo(String query) {
Map<String,Object> dataMap = new HashMap<String,Object>();
// do some op.
String empId = "abc123";
List<Long> projectIds = new ArrayList<Long>();
List<String> performanceGoals = new ArrayList<String>();
dataMap.put("empId",empId);
dataMap.put("projectIds",projectIds);
dataMap.put("performanceGoals",performanceGoals);
return dataMap;
}
The best practise is: Don't use this.
Make a Class Employee with members
public class Employee {
String id;
List<Long> projectIds;
List<String> performanceGoals;
...
}
And you method changes to:
public Employee getEmployeeInfo(String query) {
...
update for clarification why returning Map is bad in general
If your method returns:
Map<String,?> or Map<String,? extends Object> you say (in slang):
Hey look here, I am returning something. Store it in a variable called "something", because I don't say anything about the value.
If you write this method, you have to ensure, that you know every single line of code, where you work with your Map.
Lets say I would like to change employeeId from String to Integer. This will lead to really bad RuntimeExceptions and ClassCastExceptions.

In java, how can I work with objects which can be either String or List<String>? [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Maps with multiple types of values in java
I have an odd question. Maybe I'm going about this the wrong way, but let's see where this question goes :)
I would like a Map container that contains either Strings or lists of Strings. I want to enforce this rule during construction of this object so that you can't create a map with values that aren't either of those.
e.g.
class Record {
public Record(String key, Map<String,Object> attrs) {
// check that attrs only contains Objects which are Strings or List<Strings>
}
}
Other ways I have thought of to solve the problem might be...
1)
class Record {
public Record(String key, Map<String,String> attrs, Map<String,List<String>> multiAttrs) {
// ...
}
}
2)
class Record {
public Record(String key, Map<String,Value> attrs) {
// ...
}
}
class Value {
// Create some funky class that encapsulates lists.
// Perhaps returning the only element in the list if the size is 1,
// but returning the list otherwise
}
I am not immediately excited at the alternatives, but I'm just putting it there as stuff I've already considered. Really I want the distinction between Strings and List to be transparent to the user of the class.
Have you considered ListMultimap? For the single value case the list would only have one element. Multimap allows multiple elements (values) to be mapped to each key. So your method would be:
public Record(String key, ListMultimap<String, String> attrs)...
Also, since your Record seems to be another mapping, consider using Table which allows for two-key mapping.
Check out ArrayListMultimap from Google which will help with this need
You can continue calling put on this map, if you need to get the map in its simplified form you can use this method, or modify it :)
public static Map<Field, String> toSingularMap(ArrayListMultimap<Field, String> map) {
Map<Field, String> singular_map = new HashMap<Field, String>();
if (map != null && !map.isEmpty()) {
Map<Field, Collection<String>> real_map = map.asMap();
for (Iterator<Entry<Field, Collection<String>>> it = real_map
.entrySet().iterator(); it.hasNext();) {
Entry<Field, Collection<String>> entry = it.next();
Field field = entry.getKey();
Collection<String> values = entry.getValue();
String value = null;
if (values != null && !values.isEmpty()) {
ArrayList<String> list = new ArrayList<String>(values);
value = list.get(0);
}
singular_map.put(field, value);
}
}
return singular_map;
}
Or if you do not want to use an extra library, you can create a simple Wrapper class
class Wrap {
String value;
String[] values
}
and have your map use Map<String, Wrap> map, when looping you can then determine either through use of your class methods or just testing, which one of the Wrapper variables are populated
I would use only List<String>. You could maybe add some methods to allow adding a single String and wrap the passed argument using Arrays.asList(...). Using only a single type of objects will reduce the quantity of code to write and avoid many if/else.
Why not create a class
class MyFunkyValue{
private String onlyOneString;
private List<String> stringValues;
public MyFunkyValue(String s){
...
}
public MyFunkyValue(List<String>ls){
...
}
}
and use it like this:
Map<KeyClass,MyFunkyValue> m;

JavaScript objects in Java?

How do I loop over a string of item and create an object based on that?
I currently have this code:
public static Object ParseParams(String string)
{
Object params = new Object();
String[] lines = string.split("\n");
for(String line : lines)
{
String[] splittedLine = line.split("=");
params[splittedLine[0]] = splittedLine[1]; //JavaScript syntax, not Java!
}
return params;
}
The input string is in this format:
param1=value1
param2=value2
foo=bar
How do I fix the problematic line?
Edit
Sometimes the string would look like this:
foo=bar
param=1=hello
param=2=world
Would it be possible with Maps in Java to get the output like this:
foo
bar
param
1
hello
2
world
So the Maps are sometimes nested, and it you would retrieve hello by calling params.get("param").get("1");
It sounds like you want either a Map, or a JSON library.
Maps
public static Map<String, String> ParseParams(String string)
{
Map<String, String> params = new HashMap<String, String>();
String[] lines = string.split("\n");
for(String line : lines)
{
String[] splittedLine = line.split("=");
params.put(splittedLine[0], splittedLine[1]);
}
return params; // Get a param with params.get(key);
}
In general you'll want a HashMap (fast but not stored in order), but there's also a TreeMap which is slightly slower but stored in order (which can be useful sometimes).
JSON
The format JavaScript uses for objects is used as a general-purpose storage format called JSON (JavaScript Object Notation). This is only useful for storage / printing / network transmission. Internally, Java JSON libraries use maps (JavaScript interpreters probably do too).
There are several Java APIs, but StackOverflow users seem to recommend Json-lib:
public static JSONObject ParseParams(String string)
{
// Note that we need everything from the other method anyway
return JSONObject.fromObject(ParseParams(string));
}
EDIT:
You're already reaching the point where using a Map is strained. I'd suggest just using a class:
class MyStuff {
String foo;
Map<String, String> params;
}
It is possible to nest Maps, like Map<String, Map<String, String>> or Map<String, Object>, but you really should be using classes for this.

Categories

Resources