views:

450

answers:

4

Hello :-)

I am building a facebook platform web app using GWT and hosting it on App Engine.

I am adding validation code that uses supplied query string parameters in the callback url. GWT allows me to get these parameters by calling Window.Location.getParameterMap() and the returned Map is immutable.

I may be wrong however I think this problem has nothing to do with FB, GWT or App Engine specifically and is more down to my misunderstanding something about Map objects.

I don't think that my code attempts to modify the supplied Map but the error I get seems to suggest that my code is trying to modify an immutable Map.

Can someone please take a look and let me know where I am modifying an unmodifiable Map?

I would supply a stack trace but I can't find a way to get a stack trace for this to display in App Engine logs.

Thanks in advance for any and all help :-)

/**
 * Validation Test
 * To generate the signature for these arguments:
 * 1. Remove the fb_sig key and value pair.
 * 2. Remove the "fb_sig_" prefix from all of the keys.
 * 3. Sort the array alphabetically by key.
 * 4. Concatenate all key/value pairs together in the format "k=v".
 * 5. Append your secret key.
 * 6. Take the md5 hash of the whole string.
 * @param fbQueryStringParams
 * @return String
 */
public String test(Map<String,List<java.lang.String>> fbQueryStringParams) {

 String appSecret = TinyFBClient.APP_SECRET;
 String fbSig = fbQueryStringParams.get("fb_sig").get(0);
 StringBuilder sb = new StringBuilder();  
 TreeMap<String,String> sortedMap = new TreeMap<String,String>();

 // Get a Set view of the Map of query string parameters.
 Set<Map.Entry<String,List<java.lang.String>>> mapEntries = fbQueryStringParams.entrySet();

 // Iterate through the Set view, inserting into a SortedMap all Map.Entry's
 // that do not have a Key value of "fb_sig".
 Iterator<Map.Entry<String,List<java.lang.String>>> i = mapEntries.iterator();
 while(i.hasNext()) {

  Map.Entry<String,List<java.lang.String>> mapEntry = i.next();

  if(!mapEntry.getKey().equals("fb_sig")) { // 1. Remove the fb_sig key and value pair.

   sortedMap.put(mapEntry.getKey(),mapEntry.getValue().get(0)); // 3. Sort the array alphabetically by key.

  }

 }

 // Get a Set view of the Map of alphabetically sorted Map.Entry objects.
 Set<Map.Entry<String,String>> sortedMapEntries = sortedMap.entrySet();

 // Iterate through the Set view, appending the concatenated key's and value's
 // to a StringBuilder object.
 Iterator<Map.Entry<String,String>> ii = sortedMapEntries.iterator();
 while(ii.hasNext()) {

  Map.Entry<String,String> mapEntry = ii.next();

  // 4. Concatenate all key/value pairs together in the format "k=v".
  sb.append(mapEntry.getKey().replaceAll("fb_sig_","")); // 2. Remove the "fb_sig_" prefix from all of the keys.
  sb.append("=");
  sb.append(mapEntry.getValue());

 }

 sb.append(appSecret); // 5. Append your secret key.

 String md5 = DigestUtils.md5Hex(sb.toString()); // 6. Take the md5 hash of the whole string.

 // Build and return an output String for display.
 StringBuilder output = new StringBuilder();
 output.append("fbSig = "+fbSig);
 output.append("<br/>");
 output.append("md5 = "+md5);
 return output.toString();

}
+1  A: 

I don't see any unmodifiable collections.

Your code is pretty complicated. If I understood it right, then this should be equivalent. I wouldn't use Map.Entry objects and the TreeMap has a handy constructor for your needs. And finally, I'd prefer the 'forall' loop over the iterator.

public String test(Map<String,List<java.lang.String>> fbQueryStringParams) {

  String appSecret = TinyFBClient.APP_SECRET;
  String fbSig = fbQueryStringParams.get("fb_sig").get(0);
  StringBuilder sb = new StringBuilder();         
  TreeMap<String,List<String>> sortedMap = 
                    new TreeMap<String,List<String>>(fbQueryStringParams);
  sortedMap.remove("fbSig"); // remove the unwanted entry

  for (String key, sortedMap.keySet()) {
    List<String> values = sortedMap.get(key);
    String printableKey = key.replaceAll("fb_sig_",""));
    String value = "EMPTY LIST";
    if (!values.isEmpty()) {
      // This could have been your problem, you always
      // assume, all lists in the map are not empty
      value = values.get(0);
    }
    sb.append(String.format("%s=%s", printableKey, value);
  }
  sb.append(appSecret); 
  String md5 = DigestUtils.md5Hex(sb.toString()); 

  // Build and return an output String for display.
  StringBuilder output = new StringBuilder();
  output.append("fbSig = "+fbSig);
  output.append("<br/>");
  output.append("md5 = "+md5);
  return output.toString();
}

While refactoring I found one possible bug: when you create the sorted map in your code, you assume, all lists in the map are not empty. So the first empty list will cause a NPE in the first loop.

Andreas_D
Darren
But don't shout at me in case of bugs - I didn't have an IDE while refactoring ;)
Andreas_D
Only a few minor syntax bugs - impressive without an IDE.Still throws the same error. I can only see this error because this method is within an asynchronous GWT class and when the remote proceedure call fails (due I think to an exception being thrown somewhere) the exception gets caught and I output this Throwable's message. I think I need to find out how to get these exceptions to show up in the App Engine logs.Thank you very much for your help :-)
Darren
Can't you run in hosted mode to see the stack on the client side ?I guess the input Map is non-modifiable. But indeed your code does not seem to make any modification to the input parameter.So maybe, your error is completely unrelated.
David Nouls
A: 

Do a System.out.println(fbQueryStringParams.getClass()); at the start of the message (or log it or whatever you need to be able to see what it is).

If that argument is passed to you from the system it is very likely wrapped as an unmodifiable collection since they don't want you altering it.

TofuBeer
Thanks for taking time to think about this TofuBeer.The supplied Map is definitely unmodifiable as it represents the parameters in the url's query string, so it makes sense that these parameters should not be modified.It seems clear to me now that (although a little clunky) both coding solutions do not modify the Map.
Darren
I am having real problems getting useful logging to work.According to the GAE documentation at http://code.google.com/appengine/docs/java/runtime.html#Logging,"Everything the servlet writes to the standard output stream (System.out) and standard error stream (System.err) is captured by App Engine and recorded in the application logs. Lines written to the standard output stream are logged at the "INFO" level, and lines written to the standard error stream are logged at the "WARNING" level."But my attempts produce nothing. I will have to take this up on a GAE forum.
Darren
I really believe you have a clientside bug.
David Nouls
A: 

Did I understand it correctly that you are doing a Window.Location.getParameterMap in your client code and sending it to the server in a RPC call ? In that case ... the question is: is that ParameterMap serializable ? Not all implementations are in fact supported in GWT. So it might just be that your server code is not even called but that it crashes before it can send the request. Did you see any warning during GWT compilation ?

The code, although the implementation can be cleaned up and indeed you can have a NPE, is NOT modifying the supplied parameter Map or the List in the Map values. So the problem is probably somewhere else.

Why don't you run your application in hosted mode (or development mode as they call it in GWT 2.0) ?

David

David Nouls
Thanks for spending time on this David. According to the GWT source code, Window.Location.getParameterMap() returns a java.util.HashMap and according to the J2SE docs, this implements the Serializable interface.
Darren
As far as hosted mode goes, I'm not sure I understand what you mean.I use the GAE/GWT Plugin for Eclipse. Using this I can run the app locally and the code fails for other reasons - mainly it doesn't get the correct FB url, but I get full stack traces.If you mean hosted mode as in http://code.google.com/webtoolkit/doc/1.6/FAQ_DebuggingAndCompiling.html then, blimey, setting that up would be a real challenge as to be honest I don't understand most of it :-(There must be a way of getting Exceptions and stack traces to show up in GAE logs - maybe I should try that first?
Darren
I looked into the code and indeed. It is building a HashMap, but at the end it returns Collections.unmodifiableMap.Running in hosted mode is actually very simple if you use the GAE/GWT plugin. Just do a Run as Web Application on a GWT project.You should really invest time in running in hosted mode because that is the fasted way to develop and much easier to handle client side bugs. If something crashes in the client you can not log it on the server unless you do a RPC call... not a good idea.
David Nouls
In hosted mode I get this : The server is running at http://localhost:8080/com.google.gwt.user.client.rpc.SerializationException: java.util.Collections$UnmodifiableMap at org.redboffin.sandpit.client.facebook.client.FbValidationService_TypeSerializer.raiseSerializationException(transient source for org.redboffin.sandpit.client.facebook.client.FbValidationService_TypeSerializer:123) at org.redboffin.sandpit.client.facebook.client.FbValidationService_TypeSerializer.serialize(Native Method)
Darren
at com.google.gwt.user.client.rpc.impl.ClientSerializationStreamWriter.serialize(ClientSerializationStreamWriter.java:216) at com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter.writeObject(AbstractSerializationStreamWriter.java:129) at org.redboffin.sandpit.client.facebook.client.FbValidationService_Proxy.test(transient source for org.redboffin.sandpit.client.facebook.client.FbValidationService_Proxy:31) at org.redboffin.sandpit.client.Sandpit.onModuleLoad(Sandpit.java:45)
Darren
Wait a second ... what version of GWT are you using ? In 1.5 it was not possible to serialize a UnmodifiableMap.
David Nouls
I'm using GWT 1.7.1 and I have thought of a different way to achieve the same result. I pass Window.Location.getQueryString() in and just work with the string. End result is an MD5 hash comparison which all works, so the validation aspect is done. :-) Don't really understand if the UnmodifiableMap problem is a bug or not
Darren
+1  A: 

copy the Windows.Location.getParameterMap() in a HashMap and it will work:

So you send new HashMap>( Windows.Location.getParameterMap()) over RPC that works.

The problem is that unmodifiableMap is not Serializable for GWT. I know that it has a Serializable marker, but in GWT it works a little bit different. Most collection classes have a custom GWT implementation and some are not 100% compatible.

David Nouls
Hello David. I would like to vote for you but I don't know how to do it.
Darren