You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by be...@apache.org on 2009/01/10 00:33:47 UTC

svn commit: r733188 [1/2] - in /incubator/shindig/trunk/java: common/src/main/java/org/apache/shindig/common/ common/src/test/java/org/apache/shindig/common/ gadgets/src/main/java/org/apache/shindig/gadgets/http/ gadgets/src/main/java/org/apache/shindi...

Author: beaton
Date: Fri Jan  9 15:33:46 2009
New Revision: 733188

URL: http://svn.apache.org/viewvc?rev=733188&view=rev
Log:
Improve OAuth error handling.  Instead of just logging the error, we also
return the entire series of http requests and responses to the client in the
oauthErrorText response attribute.

Hopefully no client code will ever try to do something useful with the
oauthErrorText attribute; it's only intended for use by a gadget developer
who is troubleshooting.

The code is careful not to increase response size for normal responses, only
error cases provoke the more verbose response.

Added:
    incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java   (with props)
    incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java   (with props)
    incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java   (with props)
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java   (with props)
Removed:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthNoDataException.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthStoreException.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/UserVisibleOAuthException.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthProtocolExceptionTest.java
Modified:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FakeGadgetSpecFactory.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpRequestTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetTokenStoreTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthResponseParamsTest.java

Added: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java?rev=733188&view=auto
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java (added)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java Fri Jan  9 15:33:46 2009
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.common;
+
+/**
+ * A pair of any two objects.
+ */
+public class Pair<T1, T2> {
+  public final T1 one;
+  public final T2 two;
+  
+  public Pair(T1 one, T2 two) {
+    this.one = one;
+    this.two = two;
+  }
+}

Propchange: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java?rev=733188&view=auto
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java (added)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java Fri Jan  9 15:33:46 2009
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.common;
+
+/**
+ * Utility class for creating Pair objects.
+ */
+public class Pairs {
+  public static <T1, T2> Pair<T1, T2> newPair(T1 one, T2 two) {
+    return new Pair<T1, T2>(one, two);
+  }
+}

Propchange: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/Pairs.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java?rev=733188&view=auto
==============================================================================
--- incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java (added)
+++ incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java Fri Jan  9 15:33:46 2009
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.common;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class PairTest {
+
+  @Test
+  public void testPair() {
+    Pair<String, Integer> p = Pairs.newPair("one", new Integer(1));
+    assertEquals("one", p.one);
+    assertEquals(new Integer(1), p.two);
+  }
+}

Propchange: incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java Fri Jan  9 15:33:46 2009
@@ -63,7 +63,7 @@
     responseBytes = CharsetUtil.getUtf8Bytes(body);
     return this;
   }
-  
+
   /**   
    * @param responseBytes The response body. Copied when set.
    */
@@ -197,7 +197,7 @@
     return responseBytes;
   }
 
-  int getHttpStatusCode() {
+  public int getHttpStatusCode() {
     return httpStatusCode;
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java Fri Jan  9 15:33:46 2009
@@ -21,9 +21,9 @@
 
 import net.oauth.OAuthAccessor;
 
-import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthResponseParams.OAuthRequestException;
 import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
 
 /**
@@ -42,17 +42,17 @@
 
   public AccessorInfoBuilder() {
   }
-  
-  public AccessorInfo create() throws GadgetException {
+
+  public AccessorInfo create(OAuthResponseParams responseParams) throws OAuthRequestException {
     if (location == null) {
-      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, "no location");
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM, "no location");
     }
     if (consumer == null) {
-      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, "no consumer");
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM, "no consumer");
     }
-    
+
     OAuthAccessor accessor = new OAuthAccessor(consumer.getConsumer());
-        
+
     // request token/access token/token secret can all be null, for signed fetch, or if the OAuth
     // dance is just beginning
     accessor.requestToken = requestToken;
@@ -68,7 +68,7 @@
   public void setRequestToken(String requestToken) {
     this.requestToken = requestToken;
   }
-  
+
   public void setAccessToken(String accessToken) {
     this.accessToken = accessToken;
   }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java Fri Jan  9 15:33:46 2009
@@ -22,6 +22,7 @@
 import org.apache.shindig.gadgets.GadgetSpecFactory;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthResponseParams.OAuthRequestException;
 import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
 import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
@@ -76,7 +77,8 @@
    * and secret for that.  Signed fetch always sticks the parameters in the query string.
    */
   public AccessorInfo getOAuthAccessor(SecurityToken securityToken,
-      OAuthArguments arguments, OAuthClientState clientState) throws GadgetException {
+      OAuthArguments arguments, OAuthClientState clientState, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
 
     AccessorInfoBuilder accessorBuilder = new AccessorInfoBuilder();
 
@@ -84,47 +86,60 @@
     // OAuth parameters and what methods to use for their URLs?
     OAuthServiceProvider provider = null;
     if (arguments.mayUseToken()) {
-      provider = lookupSpecInfo(securityToken, arguments, accessorBuilder);
+      provider = lookupSpecInfo(securityToken, arguments, accessorBuilder, responseParams);
     } else {
       // This is plain old signed fetch.
       accessorBuilder.setParameterLocation(AccessorInfo.OAuthParamLocation.URI_QUERY);
     }
 
     // What consumer key/secret should we use?
-    ConsumerInfo consumer = store.getConsumerKeyAndSecret(
-        securityToken, arguments.getServiceName(), provider);
-    accessorBuilder.setConsumer(consumer);
+    ConsumerInfo consumer;
+    try {
+      consumer = store.getConsumerKeyAndSecret(
+          securityToken, arguments.getServiceName(), provider);
+      accessorBuilder.setConsumer(consumer);
+    } catch (GadgetException e) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to retrieve consumer key", e);
+    }
 
     // Should we use the OAuth access token?  We never do this unless the client allows it, and
     // if owner == viewer.
     if (arguments.mayUseToken()
         && securityToken.getOwnerId() != null
         && securityToken.getViewerId().equals(securityToken.getOwnerId())) {
-      lookupToken(securityToken, consumer, arguments, clientState, accessorBuilder);
+      lookupToken(securityToken, consumer, arguments, clientState, accessorBuilder, responseParams);
     }
 
-    return accessorBuilder.create();
+    return accessorBuilder.create(responseParams);
   }
 
   /**
    * Lookup information contained in the gadget spec.
    */
   private OAuthServiceProvider lookupSpecInfo(SecurityToken securityToken, OAuthArguments arguments,
-      AccessorInfoBuilder accessorBuilder) throws GadgetException {
-    GadgetSpec spec = findSpec(securityToken, arguments);
+      AccessorInfoBuilder accessorBuilder, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    GadgetSpec spec = findSpec(securityToken, arguments, responseParams);
     OAuthSpec oauthSpec = spec.getModulePrefs().getOAuthSpec();
     if (oauthSpec == null) {
-      throw oauthNotFoundEx(securityToken);
+      throw responseParams.oauthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION,
+          "Failed to retrieve OAuth URLs, spec for gadget " +
+          securityToken.getAppUrl() + " does not contain OAuth element.");
     }
     OAuthService service = oauthSpec.getServices().get(arguments.getServiceName());
     if (service == null) {
-      throw serviceNotFoundEx(securityToken, oauthSpec, arguments.getServiceName());
+      throw responseParams.oauthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION,
+          "Failed to retrieve OAuth URLs, spec for gadget does not contain OAuth service " +
+          arguments.getServiceName() + ".  Known services: " + 
+          StringUtils.join(oauthSpec.getServices().keySet(), ',') + ".");
     }
     // In theory some one could specify different parameter locations for request token and
     // access token requests, but that's probably not useful.  We just use the request token
     // rules for everything.
-    accessorBuilder.setParameterLocation(getStoreLocation(service.getRequestUrl().location));
-    accessorBuilder.setMethod(getStoreMethod(service.getRequestUrl().method));
+    accessorBuilder.setParameterLocation(
+        getStoreLocation(service.getRequestUrl().location, responseParams));
+    accessorBuilder.setMethod(getStoreMethod(service.getRequestUrl().method, responseParams));
     OAuthServiceProvider provider = new OAuthServiceProvider(
         service.getRequestUrl().url.toJavaUri().toASCIIString(),
         service.getAuthorizationUrl().toJavaUri().toASCIIString(),
@@ -147,11 +162,11 @@
    *    provider they want to add a gadget to a gadget container site, the service provider can
    *    create a preapproved request token for that site and pass it to the gadget as a user
    *    preference.
-   * @throws GadgetException
    */
   private void lookupToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
-      OAuthArguments arguments, OAuthClientState clientState, AccessorInfoBuilder accessorBuilder)
-      throws GadgetException {
+      OAuthArguments arguments, OAuthClientState clientState, AccessorInfoBuilder accessorBuilder,
+      OAuthResponseParams responseParams)
+      throws OAuthRequestException {
     if (clientState.getRequestToken() != null) {
       // We cached the request token on the client.
       accessorBuilder.setRequestToken(clientState.getRequestToken());
@@ -164,8 +179,14 @@
       accessorBuilder.setTokenExpireMillis(clientState.getTokenExpireMillis());
     } else {
       // No useful client-side state, check persistent storage
-      TokenInfo tokenInfo = store.getTokenInfo(securityToken, consumerInfo,
-          arguments.getServiceName(), arguments.getTokenName());
+      TokenInfo tokenInfo;
+      try {
+        tokenInfo = store.getTokenInfo(securityToken, consumerInfo,
+            arguments.getServiceName(), arguments.getTokenName());
+      } catch (GadgetException e) {
+        throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+            "Unable to retrieve access token", e);
+      }
       if (tokenInfo != null && tokenInfo.getAccessToken() != null) {
         // We have an access token in persistent storage, use that.
         accessorBuilder.setAccessToken(tokenInfo.getAccessToken());
@@ -181,7 +202,8 @@
     }
   }
 
-  private OAuthParamLocation getStoreLocation(Location location) throws GadgetException {
+  private OAuthParamLocation getStoreLocation(Location location,
+      OAuthResponseParams responseParams) throws OAuthRequestException {
     switch(location) {
     case HEADER:
       return OAuthParamLocation.AUTH_HEADER;
@@ -190,67 +212,62 @@
     case BODY:
       return OAuthParamLocation.POST_BODY;
     }
-    throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+    throw responseParams.oauthRequestException(OAuthError.INVALID_REQUEST,
         "Unknown parameter location " + location);
   }
 
-  private HttpMethod getStoreMethod(Method method) throws GadgetException {
+  private HttpMethod getStoreMethod(Method method, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
     switch(method) {
     case GET:
       return HttpMethod.GET;
     case POST:
       return HttpMethod.POST;
     }
-    throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
-        "Unknown method " + method);
+    throw responseParams.oauthRequestException(OAuthError.INVALID_REQUEST, "Unknown method " + method);
   }
 
-  private GadgetSpec findSpec(SecurityToken securityToken, OAuthArguments arguments)
-      throws GadgetException {
+  private GadgetSpec findSpec(SecurityToken securityToken, OAuthArguments arguments,
+      OAuthResponseParams responseParams) throws OAuthRequestException {
     try {
       return specFactory.getGadgetSpec(
           new URI(securityToken.getAppUrl()),
           arguments.getBypassSpecCache());
     } catch (URISyntaxException e) {
-      throw new UserVisibleOAuthException("could not fetch gadget spec, gadget URI invalid", e);
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Could not fetch gadget spec, gadget URI invalid.", e);
+    } catch (GadgetException e) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Could not fetch gadget spec", e);
     }
   }
 
-  private GadgetException serviceNotFoundEx(SecurityToken securityToken, OAuthSpec oauthSpec,
-      String serviceName) {
-    StringBuilder message = new StringBuilder()
-        .append("Spec for gadget ")
-        .append(securityToken.getAppUrl())
-        .append(" does not contain OAuth service ")
-        .append(serviceName)
-        .append(".  Known services: ")
-        .append(StringUtils.join(oauthSpec.getServices().keySet(), ','));
-    return new UserVisibleOAuthException(message.toString());
-  }
-
-  private GadgetException oauthNotFoundEx(SecurityToken securityToken) {
-    StringBuilder message = new StringBuilder()
-        .append("Spec for gadget ")
-        .append(securityToken.getAppUrl())
-        .append(" does not contain OAuth element.");
-    return new UserVisibleOAuthException(message.toString());
-  }
-
   /**
    * Store an access token for the given user/gadget/service/token name
    */
   public void storeTokenKeyAndSecret(SecurityToken securityToken, ConsumerInfo consumerInfo,
-      OAuthArguments arguments, TokenInfo tokenInfo) throws GadgetException {
-    store.setTokenInfo(securityToken, consumerInfo, arguments.getServiceName(),
-        arguments.getTokenName(), tokenInfo);
+      OAuthArguments arguments, TokenInfo tokenInfo, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    try {
+      store.setTokenInfo(securityToken, consumerInfo, arguments.getServiceName(),
+          arguments.getTokenName(), tokenInfo);
+    } catch (GadgetException e) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to store access token", e);
+    }
   }
 
   /**
    * Remove an access token for the given user/gadget/service/token name
    */
   public void removeToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
-      OAuthArguments arguments) throws GadgetException {
-    store.removeToken(securityToken, consumerInfo, arguments.getServiceName(),
-        arguments.getTokenName());
+      OAuthArguments arguments, OAuthResponseParams responseParams) throws OAuthRequestException {
+    try {
+      store.removeToken(securityToken, consumerInfo, arguments.getServiceName(),
+          arguments.getTokenName());
+    } catch (GadgetException e) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to remove access token", e);
+    }
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java Fri Jan  9 15:33:46 2009
@@ -21,7 +21,6 @@
 import org.apache.shindig.common.crypto.BlobCrypter;
 import org.apache.shindig.common.crypto.BlobCrypterException;
 
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -34,7 +33,7 @@
    * hour is a fairly arbitrary time limit here.
    */
   private static final int CLIENT_STATE_MAX_AGE_SECS = 3600;
-  
+
   // Our client state is encrypted key/value pairs.  These are the key names.
   private static final String REQ_TOKEN_KEY = "r";
   private static final String REQ_TOKEN_SECRET_KEY = "rs";
@@ -49,7 +48,7 @@
 
   /** Crypter to use when sending these to the client */
   private final BlobCrypter crypter;
-    
+
   /**
    * Create a new, empty client state blob.
    * 
@@ -77,10 +76,12 @@
         // Probably too old, pretend we never saw it at all.
       }
     }
-    if (state == null) state = Maps.newHashMap();
+    if (state == null) {
+      state = Maps.newHashMap();
+    }
     this.state = state; 
   }
-  
+
   /**
    * @return true if there is no state to store with the client.
    */
@@ -88,7 +89,7 @@
     // Might contain just a timestamp
     return (state.isEmpty() || (state.size() == 1 && state.containsKey("t")));
   }
-    
+
   /**
    * @return an encrypted blob of state to store with the client.
    * @throws BlobCrypterException
@@ -103,18 +104,18 @@
   public String getRequestToken() {
     return state.get(REQ_TOKEN_KEY);
   }
-  
+
   public void setRequestToken(String requestToken) {
     setNullCheck(REQ_TOKEN_KEY, requestToken);
   }
-  
+
   /**
    * OAuth request token secret
    */
   public String getRequestTokenSecret() {
     return state.get(REQ_TOKEN_SECRET_KEY);
   }
-  
+
   public void setRequestTokenSecret(String requestTokenSecret) {
     setNullCheck(REQ_TOKEN_SECRET_KEY, requestTokenSecret);
   }
@@ -125,33 +126,33 @@
   public String getAccessToken() {
     return state.get(ACCESS_TOKEN_KEY);
   }
-  
+
   public void setAccessToken(String accessToken) {
     setNullCheck(ACCESS_TOKEN_KEY, accessToken);
   }
-  
+
   /**
    * OAuth access token secret.
    */
   public String getAccessTokenSecret() {
     return state.get(ACCESS_TOKEN_SECRET_KEY);
   }
-  
+
   public void setAccessTokenSecret(String accessTokenSecret) {
     setNullCheck(ACCESS_TOKEN_SECRET_KEY, accessTokenSecret);
   }
-  
+
   /**
    * Session handle (http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html)
    */
   public String getSessionHandle() {
     return state.get(SESSION_HANDLE_KEY);
   }
-  
+
   public void setSessionHandle(String sessionHandle) {
     setNullCheck(SESSION_HANDLE_KEY, sessionHandle);
   }
-  
+
   /**
    * Expiration of access token
    * (http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html)
@@ -167,14 +168,14 @@
   public void setTokenExpireMillis(long expirationMillis) {
     setNullCheck(ACCESS_TOKEN_EXPIRATION_KEY, Long.toString(expirationMillis));
   }
-  
+
   /**
    * Owner of the OAuth token.
    */
   public String getOwner() {
     return state.get(OWNER_KEY);
   }
-  
+
   public void setOwner(String owner) {
     setNullCheck(OWNER_KEY, owner);
   }

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java?rev=733188&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java Fri Jan  9 15:33:46 2009
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.http.BasicHttpFetcher;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+
+import java.io.FileInputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  Run a simple OAuth fetcher to execute a variety of OAuth fetches and output
+ *  the result
+ *
+ *  Arguments
+ *  --consumerKey <oauth_consumer_key>
+ *  --consumerSecret <oauth_consumer_secret>
+ *  --requestorId <xoauth_requestor_id>
+ *  --accessToken <oauth_access_token>
+ *  --method <GET | POST>
+ *  --url <url>
+ *  --contentType <contentType>
+ *  --postBody <encoded post body>
+ *  --postFile <file path of post body contents>
+ *  --paramLocation <URI_QUERY | POST_BODY | AUTH_HEADER>
+ */
+public class OAuthCommandLine {
+
+  public static void main(String[] argv) throws Exception {
+    Map<String, String> params = Maps.newHashMap();
+    for (int i = 0; i < argv.length; i+=2) {
+      params.put(argv[i], argv[i+1]);
+    }
+    final String consumerKey = params.get("--consumerKey");
+    final String consumerSecret = params.get("--consumerSecret");
+    final String xOauthRequestor = params.get("--requestorId");
+    final String accessToken = params.get("--accessToken");
+    final String method = params.get("--method") == null ? "GET" :params.get("--method");
+    String url = params.get("--url");
+    String contentType = params.get("--contentType");
+    String postBody = params.get("--postBody");
+    String postFile = params.get("--postFile");
+    String paramLocation = params.get("--paramLocation");
+
+    HttpRequest request = new HttpRequest(Uri.parse(url));
+    if (contentType != null) {
+      request.setHeader("Content-Type", contentType);
+    } else {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
+    if (postBody != null) {
+      request.setPostBody(postBody.getBytes());
+    }
+    if (postFile != null) {
+      request.setPostBody(IOUtils.toByteArray(new FileInputStream(postFile)));
+    }
+
+    OAuthParamLocation paramLocationEnum = OAuthParamLocation.URI_QUERY;
+    if (paramLocation != null) {
+      paramLocationEnum = OAuthParamLocation.valueOf(paramLocation);
+    }
+
+
+    List<OAuth.Parameter> oauthParams = Lists.newArrayList();
+    UriBuilder target = new UriBuilder(Uri.parse(url));
+    String query = target.getQuery();
+    target.setQuery(null);
+    oauthParams.addAll(OAuth.decodeForm(query));
+    if (OAuth.isFormEncoded(contentType) && request.getPostBodyAsString() != null) {
+      oauthParams.addAll(OAuth.decodeForm(request.getPostBodyAsString()));
+    }
+    if (consumerKey != null) {
+      oauthParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, consumerKey));
+    }
+    if (xOauthRequestor != null) {
+      oauthParams.add(new OAuth.Parameter("xoauth_requestor_id", xOauthRequestor));
+    }
+
+    OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    accessor.accessToken = accessToken;
+    OAuthMessage message = accessor.newRequestMessage(method, target.toString(), oauthParams);
+
+    List<Map.Entry<String, String>> entryList = OAuthRequest.selectOAuthParams(message);
+
+    switch (paramLocationEnum) {
+      case AUTH_HEADER:
+        request.addHeader("Authorization", OAuthRequest.getAuthorizationHeader(entryList));
+        break;
+
+      case POST_BODY:
+        if (!OAuth.isFormEncoded(contentType)) {
+          throw new RuntimeException(
+              "OAuth param location can only be post_body if post body if of " +
+                  "type x-www-form-urlencoded");
+        }
+        String oauthData = OAuthUtil.formEncode(oauthParams);
+        if (request.getPostBodyLength() == 0) {
+          request.setPostBody(CharsetUtil.getUtf8Bytes(oauthData));
+        } else {
+          request.setPostBody((request.getPostBodyAsString() + '&' + oauthData).getBytes());
+        }
+        break;
+
+      case URI_QUERY:
+        request.setUri(Uri.parse(OAuthUtil.addParameters(request.getUri().toString(),
+            entryList)));
+        break;
+    }
+    request.setMethod(method);
+
+    HttpFetcher fetcher = new BasicHttpFetcher();
+    HttpResponse response = fetcher.fetch(request);
+
+    System.out.println("Request ------------------------------");
+    System.out.println(request.toString());
+    System.out.println("Response -----------------------------");
+    System.out.println(response.toString());
+  }
+}

Propchange: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java Fri Jan  9 15:33:46 2009
@@ -25,19 +25,24 @@
    * the gadget is incorrect.
    */
   BAD_OAUTH_CONFIGURATION,
-  
+
   /**
    * The request cannot be completed for an unspecified reason.
    */
   UNKNOWN_PROBLEM,
-  
+
   /**
    * The user is not authenticated.
    */
   UNAUTHENTICATED,
-  
+
   /**
    * The user is not the owner of the page.
    */
   NOT_OWNER,
+
+  /**
+   * The request cannot be completed because the request options were invalid.
+   */
+  INVALID_REQUEST,
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java Fri Jan  9 15:33:46 2009
@@ -27,6 +27,7 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import com.google.inject.name.Names;
 
@@ -60,6 +61,7 @@
     bind(OAuthRequest.class).toProvider(OAuthRequestProvider.class);
   }
 
+  @Singleton
   public static class OAuthCrypterProvider implements Provider<BlobCrypter> {
 
     private final BlobCrypter crypter;
@@ -96,6 +98,7 @@
     }
   }
 
+  @Singleton
   public static class OAuthStoreProvider implements Provider<OAuthStore> {
 
     private final BasicOAuthStore store;

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java Fri Jan  9 15:33:46 2009
@@ -21,10 +21,7 @@
 
 import net.oauth.OAuthMessage;
 import net.oauth.OAuthProblemException;
-import org.apache.shindig.gadgets.http.HttpResponse;
-import org.apache.shindig.gadgets.http.HttpResponseBuilder;
 
-import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -32,7 +29,7 @@
  * <a href="http://wiki.oauth.net/ProblemReporting">
  * OAuth problem reporting extension</a>
  * 
- * We divide problems into two categories:
+ * We divide problems into three categories:
  * - problems that cause us to abort the protocol.  For example, if we don't
  *   have a consumer key that the service provider accepts, we give up.
  *   
@@ -40,11 +37,11 @@
  *   example, if the service provider reports that an access token has been
  *   revoked, we throw away the token and start over.
  *   
+ * - problems that require us to refresh our access token per the OAuth
+ *   session extension protocol
+ *   
  * By default we assume most service provider errors fall into the second
  * category: we should ask for the user's permission again.
- *   
- * TODO: add a third category to cope with reauthorization per the ScalableOAuth
- * extension.
  */
 class OAuthProtocolException extends Exception {
 
@@ -58,45 +55,34 @@
                       "consumer_key_unknown",
                       "consumer_key_rejected",
                       "timestamp_refused");
-  
+
   /**
    * Problems that should force us to abort the protocol right away,
    * but we can still try to use the access token again later.
    */
   private static Set<String> temporaryProblems = 
       ImmutableSet.of("consumer_key_refused");
-  
+
   /**
    * Problems that should have us try to refresh the access token.
    */
   private static Set<String> extensionProblems =
       ImmutableSet.of("access_token_expired");
-  
-  private final String problemCode;
-  private final String problemText;
-  
+
   private final boolean canRetry;
 
   private final boolean startFromScratch;
-  
+
   private final boolean canExtend;
-  
-  OAuthProtocolException(boolean canRetry) {
-    this.problemCode = null;
-    this.problemText = null;
-    this.canRetry = canRetry;
-    this.startFromScratch = false;
-    this.canExtend = false;
-  }
-  
+
+  private final String problemCode;
+
   public OAuthProtocolException(OAuthMessage reply) {
     String problem = OAuthUtil.getParameter(reply, OAuthProblemException.OAUTH_PROBLEM);
     if (problem == null) {
-      throw new IllegalArgumentException(
-          "No problem reported for OAuthProtocolException");
+      throw new IllegalArgumentException("No problem reported for OAuthProtocolException");
     }
     this.problemCode = problem;
-    this.problemText = OAuthUtil.getParameter(reply, "oauth_problem_advice");
     if (fatalProblems.contains(problem)) {
       startFromScratch = true;
       canRetry = false;
@@ -122,8 +108,6 @@
    * @param status HTTP status code, assumed to be between 400 and 499 inclusive
    */
   public OAuthProtocolException(int status) {
-    problemCode = Integer.toString(status);
-    problemText = null;
     if (status == 401) {
       startFromScratch = true;
       canRetry = true;
@@ -132,6 +116,7 @@
       canRetry = false;
     }
     canExtend = false;
+    problemCode = null;
   }
 
   /**
@@ -141,7 +126,7 @@
   public boolean startFromScratch() {
     return startFromScratch;
   }
-  
+
   /**
    * @return true if we think we can make progress by attempting the protocol
    * flow again (which may require starting from scratch).
@@ -149,7 +134,7 @@
   public boolean canRetry() {
     return canRetry;
   }
-  
+
   /**
    * @return true if we think we can make progress by attempting to extend the lifetime of the
    * access token.
@@ -157,14 +142,11 @@
   public boolean canExtend() {
     return canExtend;
   }
-  
-  public HttpResponse getResponseForGadget() {
-    return new HttpResponseBuilder()
-        .setHttpStatusCode(0)
-        // Inch towards opensocial-0.8: this is very much an experiment, don't
-        // hesitate to change it if you've got something better.
-        .setMetadata("oauthError", problemCode)
-        .setMetadata("oauthErrorText", problemText)
-        .create();
+
+  /**
+   * @return the OAuth problem code (from the problem reporting extension).
+   */
+  public String getProblemCode() {
+    return problemCode;
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java Fri Jan  9 15:33:46 2009
@@ -20,14 +20,13 @@
 import org.apache.shindig.common.uri.UriBuilder;
 import org.apache.shindig.common.util.CharsetUtil;
 import org.apache.shindig.gadgets.GadgetException;
-import org.apache.shindig.gadgets.RequestSigningException;
-import org.apache.shindig.gadgets.http.BasicHttpFetcher;
 import org.apache.shindig.gadgets.http.HttpFetcher;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthResponseParams.OAuthRequestException;
 import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
 
 import com.google.common.collect.Lists;
@@ -35,22 +34,17 @@
 
 import net.oauth.OAuth;
 import net.oauth.OAuthAccessor;
-import net.oauth.OAuthConsumer;
 import net.oauth.OAuthException;
 import net.oauth.OAuthMessage;
 import net.oauth.OAuthProblemException;
 import net.oauth.OAuth.Parameter;
 
-import org.apache.commons.io.IOUtils;
 import org.json.JSONObject;
 
-import java.io.FileInputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 import java.util.regex.Pattern;
 
 /**
@@ -70,9 +64,6 @@
  */
 public class OAuthRequest {
 
-  // Logger
-  private static final Logger logger = Logger.getLogger(OAuthRequest.class.getName());
-
   // Maximum number of attempts at the protocol before giving up.
   private static final int MAX_ATTEMPTS = 2;
 
@@ -106,6 +97,9 @@
    */
   protected final OAuthFetcherConfig fetcherConfig;
 
+  /**
+   * Next fetcher to use in chain.
+   */
   private final HttpFetcher fetcher;
 
   /**
@@ -145,31 +139,70 @@
   }
 
   /**
-   * Retrieves metadata from our persistent store.
+   * OAuth authenticated fetch.
    */
-  private void lookupOAuthMetadata() throws GadgetException {
-    accessorInfo = fetcherConfig.getTokenStore().getOAuthAccessor(
-        realRequest.getSecurityToken(), realRequest.getOAuthArguments(), clientState);
-  }
-
-  public HttpResponse fetch(HttpRequest request) throws GadgetException {
-    this.clientState = new OAuthClientState(
+  public HttpResponse fetch(HttpRequest request) {
+    realRequest = request;
+    clientState = new OAuthClientState(
         fetcherConfig.getStateCrypter(),
         request.getOAuthArguments().getOrigClientState());
-    this.responseParams = new OAuthResponseParams(fetcherConfig.getStateCrypter());
-    this.realRequest = request;
-
-    HttpResponse response = null;
+    responseParams = new OAuthResponseParams(request.getSecurityToken(), request,
+        fetcherConfig.getStateCrypter());
+    try {
+      return fetchNoThrow();
+    } catch (RuntimeException e) {
+      // We log here to record the request/response pairs that created the failure.
+      responseParams.logDetailedWarning("OAuth fetch unexpected fatal error", e);
+      throw e;
+    }
+  }
 
+  /**
+   * Fetch data and build a response to return to the client.  We try to always return something
+   * reasonable to the calling app no matter what kind of madness happens along the way.  If an
+   * unchecked exception occurs, well, then the client is out of luck.
+   */
+  private HttpResponse fetchNoThrow() {
+    HttpResponseBuilder response = null;    
     try {
-      lookupOAuthMetadata();
-    } catch (GadgetException e) {
-      responseParams.setError(OAuthError.BAD_OAUTH_CONFIGURATION);
-      return buildErrorResponse(e);
+      accessorInfo = fetcherConfig.getTokenStore().getOAuthAccessor(
+          realRequest.getSecurityToken(), realRequest.getOAuthArguments(), clientState,
+          responseParams);
+      response = fetchWithRetry();     
+    } catch (OAuthRequestException e) {
+      // No data for us.
+      responseParams.logDetailedWarning("OAuth fetch fatal error", e);
+      responseParams.setSendTraceToClient(true);
+      if (response == null) {
+        response = new HttpResponseBuilder()
+            .setHttpStatusCode(HttpResponse.SC_FORBIDDEN);
+        responseParams.addToResponse(response);
+        return response.create();
+      }
+    }
+
+    // OK, got some data back, annotate it as necessary.
+    if (response.getHttpStatusCode() >= 400) {
+      responseParams.logDetailedWarning("OAuth fetch fatal error");
+      responseParams.setSendTraceToClient(true);
+    } else if (responseParams.getAznUrl() != null && responseParams.sawErrorResponse()) {
+      responseParams.logDetailedWarning("OAuth fetch error, reprompting for user approval");
+      responseParams.setSendTraceToClient(true);
     }
 
+    responseParams.addToResponse(response);
+
+    return response.create();
+  }
+
+  /**
+   * Fetch data, retrying in the event that that the service provider returns an error and we think
+   * we can recover by restarting the protocol flow.
+   */
+  private HttpResponseBuilder fetchWithRetry() throws OAuthRequestException {
     int attempts = 0;
     boolean retry;
+    HttpResponseBuilder response = null;
     do {
       retry = false;
       ++attempts;
@@ -178,40 +211,26 @@
       } catch (OAuthProtocolException pe) {
         retry = handleProtocolException(pe, attempts);
         if (!retry) {
-          response = pe.getResponseForGadget();
+          if (pe.getProblemCode() != null) {
+            throw responseParams.oauthRequestException(pe.getProblemCode(),
+                "Service provider rejected request", pe);
+          } else {
+            throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+                "Service provider rejected request", pe);
+          }
         }
-      } catch (UserVisibleOAuthException e) {
-        responseParams.setError(e.getOAuthErrorCode());
-        return buildErrorResponse(e);
       }
     } while (retry);
-
-    if (response == null) {
-      throw new GadgetException(
-          GadgetException.Code.INTERNAL_SERVER_ERROR,
-          "No response for OAuth fetch to " + realRequest.getUri());
-    }
     return response;
   }
 
-  private HttpResponse buildErrorResponse(GadgetException e) {
-    if (responseParams.getError() == null) {
-      responseParams.setError(OAuthError.UNKNOWN_PROBLEM);
-    }
-    if (responseParams.getErrorText() == null && (e instanceof UserVisibleOAuthException)) {
-      responseParams.setErrorText(e.getMessage());
-    }
-    logger.log(Level.WARNING, "OAuth error", e);
-    return buildNonDataResponse(403);
-  }
-
-  private boolean handleProtocolException(
-      OAuthProtocolException pe, int attempts) throws GadgetException {
+  private boolean handleProtocolException(OAuthProtocolException pe, int attempts)
+      throws OAuthRequestException {
     if (pe.canExtend()) {
       accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_FORCE_EXPIRE);
     } else if (pe.startFromScratch()) {
       fetcherConfig.getTokenStore().removeToken(realRequest.getSecurityToken(),
-          accessorInfo.getConsumer(), realRequest.getOAuthArguments());
+          accessorInfo.getConsumer(), realRequest.getOAuthArguments(), responseParams);
       accessorInfo.getAccessor().accessToken = null;
       accessorInfo.getAccessor().requestToken = null;
       accessorInfo.getAccessor().tokenSecret = null;
@@ -221,8 +240,14 @@
     return (attempts < MAX_ATTEMPTS && pe.canRetry());
   }
 
-  private HttpResponse attemptFetch()
-      throws GadgetException, OAuthProtocolException {
+  /**
+   * Does one of the following:
+   * 1) Sends a request token request, and returns an approval URL to the calling app.
+   * 2) Sends an access token request to swap a request token for an access token, and then asks
+   *    for data from the service provider.
+   * 3) Asks for data from the service provider.
+   */
+  private HttpResponseBuilder attemptFetch() throws OAuthRequestException, OAuthProtocolException {
     if (needApproval()) {
       // This is section 6.1 of the OAuth spec.
       checkCanApprove();
@@ -232,7 +257,9 @@
       buildAznUrl();
       // break out of the content fetching chain, we need permission from
       // the user to do this
-      return buildOAuthApprovalResponse();
+      return new HttpResponseBuilder()
+         .setHttpStatusCode(HttpResponse.SC_OK)
+         .setStrictNoCache();
     } else if (needAccessToken()) {
       // This is section 6.3 of the OAuth spec
       checkCanApprove();
@@ -255,61 +282,55 @@
   /**
    * Make sure the user is authorized to approve access tokens.  At the moment
    * we restrict this to page owner's viewing their own pages.
-   *
-   * @throws GadgetException
    */
-  private void checkCanApprove() throws GadgetException {
+  private void checkCanApprove() throws OAuthRequestException {
     String pageOwner = realRequest.getSecurityToken().getOwnerId();
     String pageViewer = realRequest.getSecurityToken().getViewerId();
     String stateOwner = clientState.getOwner();
     if (pageOwner == null) {
-      throw new UserVisibleOAuthException(OAuthError.UNAUTHENTICATED, "Unauthenticated");
+      throw responseParams.oauthRequestException(OAuthError.UNAUTHENTICATED, "Unauthenticated");
     }
     if (!pageOwner.equals(pageViewer)) {
-      throw new UserVisibleOAuthException(OAuthError.NOT_OWNER,
+      throw responseParams.oauthRequestException(OAuthError.NOT_OWNER,
           "Only page owners can grant OAuth approval");
     }
     if (stateOwner != null && !stateOwner.equals(pageOwner)) {
-      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
-          "Client state belongs to a different person.");
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Client state belongs to a different person " + 
+          "(state owner=" + stateOwner + ", pageOwner=" + pageOwner + ")");
     }
   }
 
-  private void fetchRequestToken() throws GadgetException, OAuthProtocolException {
-    try {
-      OAuthAccessor accessor = accessorInfo.getAccessor();
-      HttpRequest request = new HttpRequest(
-          Uri.parse(accessor.consumer.serviceProvider.requestTokenURL));
-      request.setMethod(accessorInfo.getHttpMethod().toString());
-      if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
-        request.setHeader("Content-Type", OAuth.FORM_ENCODED);
-      }
+  private void fetchRequestToken() throws OAuthRequestException, OAuthProtocolException {
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    HttpRequest request = new HttpRequest(
+        Uri.parse(accessor.consumer.serviceProvider.requestTokenURL));
+    request.setMethod(accessorInfo.getHttpMethod().toString());
+    if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
 
-      HttpRequest signed = sanitizeAndSign(request, null);
+    HttpRequest signed = sanitizeAndSign(request, null);
 
-      OAuthMessage reply = sendOAuthMessage(signed);
+    OAuthMessage reply = sendOAuthMessage(signed);
 
-      accessor.requestToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
-      accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
-    } catch (OAuthException e) {
-      throw new UserVisibleOAuthException(e.getMessage(), e);
-    }
+    accessor.requestToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
+    accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
   }
 
   /**
    * Strip out any owner or viewer identity information passed by the client.
-   *
-   * @throws RequestSigningException
    */
-  private List<Parameter> sanitize(List<Parameter> params)
-      throws RequestSigningException {
+  private List<Parameter> sanitize(List<Parameter> params) throws OAuthRequestException {
     ArrayList<Parameter> list = Lists.newArrayList();
     for (Parameter p : params) {
       String name = p.getKey();
       if (allowParam(name)) {
         list.add(p);
       } else {
-        throw new RequestSigningException("invalid parameter name " + name);
+        throw responseParams.oauthRequestException(OAuthError.INVALID_REQUEST,
+            "invalid parameter name " + name +
+            ", applications may not override opensocial or oauth parameters");           
       }
     }
     return list;
@@ -372,8 +393,7 @@
         Long.toString(fetcherConfig.getClock().currentTimeMillis() / 1000)));
   }
 
-  private static String getAuthorizationHeader(
-      List<Map.Entry<String, String>> oauthParams) {
+  static String getAuthorizationHeader(List<Map.Entry<String, String>> oauthParams) {
     StringBuilder result = new StringBuilder("OAuth ");
 
     boolean first = true;
@@ -392,17 +412,17 @@
   }
 
 
-  /*
-    Start with an HttpRequest.
-    Throw if there are any attacks in the query.
-    Throw if there are any attacks in the post body.
-    Build up OAuth parameter list
-    Sign it.
-    Add OAuth parameters to new request
-    Send it.
-  */
+  /**
+   * Start with an HttpRequest.
+   * Throw if there are any attacks in the query.
+   * Throw if there are any attacks in the post body.
+   * Build up OAuth parameter list.
+   * Sign it.
+   * Add OAuth parameters to new request.
+   * Send it.
+   */
   public HttpRequest sanitizeAndSign(HttpRequest base, List<Parameter> params)
-      throws GadgetException {
+      throws OAuthRequestException {
     if (params == null) {
       params = Lists.newArrayList();
     }
@@ -426,12 +446,13 @@
       oauthHttpRequest.setFollowRedirects(false);
       return oauthHttpRequest;
     } catch (OAuthException e) {
-      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Error signing message", e);
     }
   }
 
   private HttpRequest createHttpRequest(HttpRequest base,
-      List<Map.Entry<String, String>> oauthParams) throws GadgetException {
+      List<Map.Entry<String, String>> oauthParams) throws OAuthRequestException {
 
     OAuthParamLocation paramLocation = accessorInfo.getParamLocation();
 
@@ -457,9 +478,9 @@
       case POST_BODY:
         String contentType = result.getHeader("Content-Type");
         if (!OAuth.isFormEncoded(contentType)) {
-          throw new UserVisibleOAuthException(
+          throw responseParams.oauthRequestException(OAuthError.INVALID_REQUEST,
               "OAuth param location can only be post_body if post body if of " +
-              "type x-www-form-urlencoded");
+              "type x-www-form-urlencoded");              
         }
         String oauthData = OAuthUtil.formEncode(oauthParams);
         if (result.getPostBodyLength() == 0) {
@@ -481,23 +502,22 @@
    * Sends OAuth request token and access token messages.
    */
   private OAuthMessage sendOAuthMessage(HttpRequest request)
-      throws GadgetException, OAuthProtocolException, OAuthProblemException {
-    HttpResponse response = fetcher.fetch(request);
-    boolean done = false;
-    try {
-      checkForProtocolProblem(response);
-      OAuthMessage reply = new OAuthMessage(null, null, null);
-
-      reply.addParameters(OAuth.decodeForm(response.getResponseAsString()));
-      reply = parseAuthHeader(reply, response);
-      OAuthUtil.requireParameters(reply, OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET);
-      done = true;
-      return reply;
-    } finally {
-      if (!done) {
-        logServiceProviderError(request, response);
-      }
+      throws OAuthRequestException, OAuthProtocolException {
+    HttpResponse response = fetchFromServer(request);
+    checkForProtocolProblem(response);
+    OAuthMessage reply = new OAuthMessage(null, null, null);
+
+    reply.addParameters(OAuth.decodeForm(response.getResponseAsString()));
+    reply = parseAuthHeader(reply, response);
+    if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN) == null) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "No oauth_token returned from service provider");
+    }
+    if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET) == null) {
+      throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "No oauth_token_secret returned from service provider");  
     }
+    return reply;
   }
 
   /**
@@ -550,17 +570,6 @@
     responseParams.setAznUrl(azn.toString());
   }
 
-  private HttpResponse buildOAuthApprovalResponse() {
-    return buildNonDataResponse(200);
-  }
-
-  private HttpResponse buildNonDataResponse(int status) {
-    HttpResponseBuilder response = new HttpResponseBuilder().setHttpStatusCode(status);
-    responseParams.addToResponse(response);
-    response.setStrictNoCache();
-    return response.create();
-  }
-
   /**
    * Do we need to exchange a request token for an access token?
    */
@@ -583,87 +592,79 @@
 
   /**
    * Implements section 6.3 of the OAuth spec.
-   * @throws OAuthProtocolException
    */
-  private void exchangeRequestToken()
-      throws GadgetException, OAuthProtocolException {
-    try {
-      if (accessorInfo.getAccessor().accessToken != null) {
-        // session extension per
-        // http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html
-        accessorInfo.getAccessor().requestToken = accessorInfo.getAccessor().accessToken;
-        accessorInfo.getAccessor().accessToken = null;
-      }
-      OAuthAccessor accessor = accessorInfo.getAccessor();
-      Uri accessTokenUri = Uri.parse(accessor.consumer.serviceProvider.accessTokenURL);
-      HttpRequest request = new HttpRequest(accessTokenUri);
-      request.setMethod(accessorInfo.getHttpMethod().toString());
-      if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
-        request.setHeader("Content-Type", OAuth.FORM_ENCODED);
-      }
+  private void exchangeRequestToken() throws OAuthRequestException, OAuthProtocolException {
+    if (accessorInfo.getAccessor().accessToken != null) {
+      // session extension per
+      // http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html
+      accessorInfo.getAccessor().requestToken = accessorInfo.getAccessor().accessToken;
+      accessorInfo.getAccessor().accessToken = null;
+    }
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    Uri accessTokenUri = Uri.parse(accessor.consumer.serviceProvider.accessTokenURL);
+    HttpRequest request = new HttpRequest(accessTokenUri);
+    request.setMethod(accessorInfo.getHttpMethod().toString());
+    if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
 
-      List<Parameter> msgParams = Lists.newArrayList();
-      msgParams.add(new Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
-      if (accessorInfo.getSessionHandle() != null) {
-        msgParams.add(new Parameter(OAUTH_SESSION_HANDLE, accessorInfo.getSessionHandle()));
-      }
+    List<Parameter> msgParams = Lists.newArrayList();
+    msgParams.add(new Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
+    if (accessorInfo.getSessionHandle() != null) {
+      msgParams.add(new Parameter(OAUTH_SESSION_HANDLE, accessorInfo.getSessionHandle()));
+    }
 
-      HttpRequest signed = sanitizeAndSign(request, msgParams);
+    HttpRequest signed = sanitizeAndSign(request, msgParams);
 
-      OAuthMessage reply = sendOAuthMessage(signed);
+    OAuthMessage reply = sendOAuthMessage(signed);
 
-      accessor.accessToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
-      accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
-      accessorInfo.setSessionHandle(OAuthUtil.getParameter(reply, OAUTH_SESSION_HANDLE));
-      accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
-      if (OAuthUtil.getParameter(reply, OAUTH_EXPIRES_IN) != null) {
-        try {
-          int expireSecs = Integer.parseInt(OAuthUtil.getParameter(reply, OAUTH_EXPIRES_IN));
-          long expireMillis = fetcherConfig.getClock().currentTimeMillis() + expireSecs * 1000;
-          accessorInfo.setTokenExpireMillis(expireMillis);
-        } catch (NumberFormatException e) {
-          // Hrm.  Bogus server.  We can safely ignore this, we'll just wait for the server to
-          // tell us when the access token has expired.
-          logger.log(Level.WARNING, "server returned bogus expiration: " + reply);
-        }
+    accessor.accessToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
+    accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
+    accessorInfo.setSessionHandle(OAuthUtil.getParameter(reply, OAUTH_SESSION_HANDLE));
+    accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
+    if (OAuthUtil.getParameter(reply, OAUTH_EXPIRES_IN) != null) {
+      try {
+        int expireSecs = Integer.parseInt(OAuthUtil.getParameter(reply, OAUTH_EXPIRES_IN));
+        long expireMillis = fetcherConfig.getClock().currentTimeMillis() + expireSecs * 1000;
+        accessorInfo.setTokenExpireMillis(expireMillis);
+      } catch (NumberFormatException e) {
+        // Hrm.  Bogus server.  We can safely ignore this, we'll just wait for the server to
+        // tell us when the access token has expired.
+        responseParams.logDetailedWarning("server returned bogus expiration");
       }
+    }
 
-      // Clients may want to retrieve extra information returned with the access token.  Several
-      // OAuth service providers (e.g. Yahoo, NetFlix) return a user id along with the access
-      // token, and the user id is required to use their APIs.  Clients signal that they need this
-      // extra data by sending a fetch request for the access token URL.
-      //
-      // We don't return oauth* parameters from the response, because we know how to handle those
-      // ourselves and some of them (such as oauth_token_secret) aren't supposed to be sent to the
-      // client.
-      //
-      // Note that this data is not stored server-side.  Clients need to cache these user-ids or
-      // other data themselves, probably in user prefs, if they expect to need the data in the
-      // future.
-      if (accessTokenUri.equals(realRequest.getUri())) {
-        accessTokenData = Maps.newHashMap();
-        for (Entry<String, String> param : OAuthUtil.getParameters(reply)) {
-          if (!param.getKey().startsWith("oauth")) {
-            accessTokenData.put(param.getKey(), param.getValue());
-          }
+    // Clients may want to retrieve extra information returned with the access token.  Several
+    // OAuth service providers (e.g. Yahoo, NetFlix) return a user id along with the access
+    // token, and the user id is required to use their APIs.  Clients signal that they need this
+    // extra data by sending a fetch request for the access token URL.
+    //
+    // We don't return oauth* parameters from the response, because we know how to handle those
+    // ourselves and some of them (such as oauth_token_secret) aren't supposed to be sent to the
+    // client.
+    //
+    // Note that this data is not stored server-side.  Clients need to cache these user-ids or
+    // other data themselves, probably in user prefs, if they expect to need the data in the
+    // future.
+    if (accessTokenUri.equals(realRequest.getUri())) {
+      accessTokenData = Maps.newHashMap();
+      for (Entry<String, String> param : OAuthUtil.getParameters(reply)) {
+        if (!param.getKey().startsWith("oauth")) {
+          accessTokenData.put(param.getKey(), param.getValue());
         }
       }
-    } catch (OAuthException e) {
-      throw new UserVisibleOAuthException(e.getMessage(), e);
     }
   }
 
   /**
    * Save off our new token and secret to the persistent store.
-   *
-   * @throws GadgetException
    */
-  private void saveAccessToken() throws GadgetException {
+  private void saveAccessToken() throws OAuthRequestException {
     OAuthAccessor accessor = accessorInfo.getAccessor();
     TokenInfo tokenInfo = new TokenInfo(accessor.accessToken, accessor.tokenSecret,
         accessorInfo.getSessionHandle(), accessorInfo.getTokenExpireMillis());
     fetcherConfig.getTokenStore().storeTokenKeyAndSecret(realRequest.getSecurityToken(),
-        accessorInfo.getConsumer(), realRequest.getOAuthArguments(), tokenInfo);
+        accessorInfo.getConsumer(), realRequest.getOAuthArguments(), tokenInfo, responseParams);
   }
 
   /**
@@ -684,7 +685,7 @@
    * @throws OAuthProtocolException if the service provider returns an OAuth
    * related error instead of user data.
    */
-  private HttpResponse fetchData() throws GadgetException, OAuthProtocolException {
+  private HttpResponseBuilder fetchData() throws OAuthRequestException, OAuthProtocolException {
     HttpResponseBuilder builder = null;
     if (accessTokenData != null) {
       // This is a request for access token data, return it.
@@ -692,19 +693,29 @@
     } else {
       HttpRequest signed = sanitizeAndSign(realRequest, null);
 
-      HttpResponse response = fetcher.fetch(signed);
+      HttpResponse response = fetchFromServer(signed);
 
-      try {
-        checkForProtocolProblem(response);
-      } catch (OAuthProtocolException e) {
-        logServiceProviderError(signed, response);
-        throw e;
-      }
+      checkForProtocolProblem(response);
       builder = new HttpResponseBuilder(response);
     }
-    // Track metadata on the response
-    responseParams.addToResponse(builder);
-    return builder.create();
+    return builder;
+  }
+
+  private HttpResponse fetchFromServer(HttpRequest request) throws OAuthRequestException {
+    HttpResponse response = null;
+    try {
+      response = fetcher.fetch(request);
+      if (response == null) {
+        throw responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+            "No response from server");
+      }
+      return response;
+    } catch (GadgetException e) {
+      throw responseParams.oauthRequestException(
+          OAuthError.UNKNOWN_PROBLEM, "No response from server", e);
+    } finally {
+      responseParams.addRequestTrace(request, response);
+    }
   }
 
   /**
@@ -779,9 +790,7 @@
    *
    * @return a list that contains only the oauth_related parameters.
    */
-  private static List<Map.Entry<String, String>>
-      selectOAuthParams(OAuthMessage message) {
-
+  static List<Map.Entry<String, String>> selectOAuthParams(OAuthMessage message) {
     List<Map.Entry<String, String>> result = Lists.newArrayList();
     for (Map.Entry<String, String> param : OAuthUtil.getParameters(message)) {
       if (isContainerInjectedParameter(param.getKey())) {
@@ -795,119 +804,4 @@
     key = key.toLowerCase();
     return key.startsWith("oauth") || key.startsWith("xoauth") || key.startsWith("opensocial");
   }
-
-
-  /** Logging for errors that service providers return to us, useful for integration problems */
-  private void logServiceProviderError(HttpRequest request, HttpResponse response) {
-    logger.log(Level.INFO, "OAuth request failed:\n" + request + "\nresponse:\n" + response);
-  }
-
-  /**
-   *  Run a simple OAuth fetcher to execute a variety of OAuth fetches and output
-   *  the result
-   *
-   *  Arguments
-   *  --consumerKey <oauth_consumer_key>
-   *  --consumerSecret <oauth_consumer_secret>
-   *  --requestorId <xoauth_requestor_id>
-   *  --accessToken <oauth_access_token>
-   *  --method <GET | POST>
-   *  --url <url>
-   *  --contentType <contentType>
-   *  --postBody <encoded post body>
-   *  --postFile <file path of post body contents>
-   *  --paramLocation <URI_QUERY | POST_BODY | AUTH_HEADER>
-   *
-   */
-  public static void main(String[] argv) throws Exception {
-    Map<String, String> params = Maps.newHashMap();
-    for (int i = 0; i < argv.length; i+=2) {
-      params.put(argv[i], argv[i+1]);
-    }
-    final String consumerKey = params.get("--consumerKey");
-    final String consumerSecret = params.get("--consumerSecret");
-    final String xOauthRequestor = params.get("--requestorId");
-    final String accessToken = params.get("--accessToken");
-    final String method = params.get("--method") == null ? "GET" :params.get("--method");
-    String url = params.get("--url");
-    String contentType = params.get("--contentType");
-    String postBody = params.get("--postBody");
-    String postFile = params.get("--postFile");
-    String paramLocation = params.get("--paramLocation");
-
-    HttpRequest request = new HttpRequest(Uri.parse(url));
-    if (contentType != null) {
-      request.setHeader("Content-Type", contentType);
-    } else {
-      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
-    }
-    if (postBody != null) {
-      request.setPostBody(postBody.getBytes());
-    }
-    if (postFile != null) {
-      request.setPostBody(IOUtils.toByteArray(new FileInputStream(postFile)));
-    }
-
-    OAuthParamLocation paramLocationEnum = OAuthParamLocation.URI_QUERY;
-    if (paramLocation != null) {
-      paramLocationEnum = OAuthParamLocation.valueOf(paramLocation);
-    }
-
-
-    List<OAuth.Parameter> oauthParams = Lists.newArrayList();
-    UriBuilder target = new UriBuilder(Uri.parse(url));
-    String query = target.getQuery();
-    target.setQuery(null);
-    oauthParams.addAll(OAuth.decodeForm(query));
-    if (OAuth.isFormEncoded(contentType) && request.getPostBodyAsString() != null) {
-      oauthParams.addAll(OAuth.decodeForm(request.getPostBodyAsString()));
-    }
-    if (consumerKey != null) {
-      oauthParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, consumerKey));
-    }
-    if (xOauthRequestor != null) {
-      oauthParams.add(new OAuth.Parameter("xoauth_requestor_id", xOauthRequestor));
-    }
-
-    OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
-    OAuthAccessor accessor = new OAuthAccessor(consumer);
-    accessor.accessToken = accessToken;
-    OAuthMessage message = accessor.newRequestMessage(method, target.toString(), oauthParams);
-
-    List<Map.Entry<String, String>> entryList = selectOAuthParams(message);
-
-    switch (paramLocationEnum) {
-      case AUTH_HEADER:
-        request.addHeader("Authorization", getAuthorizationHeader(entryList));
-        break;
-
-      case POST_BODY:
-        if (!OAuth.isFormEncoded(contentType)) {
-          throw new UserVisibleOAuthException(
-              "OAuth param location can only be post_body if post body if of " +
-                  "type x-www-form-urlencoded");
-        }
-        String oauthData = OAuthUtil.formEncode(oauthParams);
-        if (request.getPostBodyLength() == 0) {
-          request.setPostBody(CharsetUtil.getUtf8Bytes(oauthData));
-        } else {
-          request.setPostBody((request.getPostBodyAsString() + '&' + oauthData).getBytes());
-        }
-        break;
-
-      case URI_QUERY:
-        request.setUri(Uri.parse(OAuthUtil.addParameters(request.getUri().toString(),
-            entryList)));
-        break;
-    }
-    request.setMethod(method);
-
-    HttpFetcher fetcher = new BasicHttpFetcher();
-    HttpResponse response = fetcher.fetch(request);
-
-    System.out.println("Request ------------------------------");
-    System.out.println(request.toString());
-    System.out.println("Response -----------------------------");
-    System.out.println(response.toString());
-  }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java Fri Jan  9 15:33:46 2009
@@ -19,45 +19,164 @@
 
 package org.apache.shindig.gadgets.oauth;
 
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.Pairs;
 import org.apache.shindig.common.crypto.BlobCrypter;
 import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.common.util.Check;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
 
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  * Container for OAuth specific data to include in the response to the client.
  */
 public class OAuthResponseParams {
-  
+
+  private static final Logger logger = Logger.getLogger(OAuthResponseParams.class.getName());
+
+  // Finds the values of sensitive response params: oauth_token_secret and oauth_session_handle
+  private static final Pattern REMOVE_SECRETS =
+      Pattern.compile("(?<=(oauth_token_secret|oauth_session_handle)=)[^=& \t\r\n]*");
+
   // names for the JSON values we return to the client
   public static final String CLIENT_STATE = "oauthState";
   public static final String APPROVAL_URL = "oauthApprovalUrl";
   public static final String ERROR_CODE = "oauthError";
   public static final String ERROR_TEXT = "oauthErrorText";
-  
+
   /**
    * Transient state we want to cache client side.
    */
-  private OAuthClientState newClientState;
-  
+  private final OAuthClientState newClientState;
+
+  /**
+   * Security token used to authenticate request.
+   */
+  private final SecurityToken securityToken;
+
+  /**
+   * Original request from client.
+   */
+  private final HttpRequest originalRequest;
+
+  /**
+   * Request/response pairs we sent onward.
+   */
+  private final List<Pair<HttpRequest, HttpResponse>> requestTrace = Lists.newArrayList();
+
   /**
    * Authorization URL for the client.
    */
   private String aznUrl;
-  
+
   /**
    * Error code for the client.
    */
-  private OAuthError error;
-  
+  private String error;
+
   /**
    * Error text for the client.
    */
   private String errorText;
-  
-  public OAuthResponseParams(BlobCrypter stateCrypter) {
+
+  /**
+   * Whether we should include the request trace in the response to the application.
+   *
+   * It might be nice to make this configurable based on options passed to makeRequest.  For now
+   * we use some heuristics to figure it out.
+   */
+  private boolean sendTraceToClient;
+
+  /**
+   * Create response parameters.
+   */
+  public OAuthResponseParams(SecurityToken securityToken, HttpRequest originalRequest,
+      BlobCrypter stateCrypter) {
+    this.securityToken = securityToken;
+    this.originalRequest = originalRequest;
     newClientState = new OAuthClientState(stateCrypter);
   }
-  
+
+  /**
+   * Log a warning message that includes the details of the request.
+   */
+  public void logDetailedWarning(String note) {
+    logger.log(Level.WARNING, note + "\n" + getDetails());
+  }
+
+  /**
+   * Log a warning message that includes the details of the request and the thrown exception.
+   */
+  public void logDetailedWarning(String note, Throwable cause) {
+    logger.log(Level.WARNING, note + "\n" + getDetails(), cause);
+  }
+
+  /**
+   * Add a request/response pair to our trace of actions associated with this request.
+   */
+  public void addRequestTrace(HttpRequest request, HttpResponse response) {
+    this.requestTrace.add(Pairs.newPair(request, response));
+  }
+
+  /**
+   * @return true if the target server returned an error at some point during the request
+   */
+  public boolean sawErrorResponse() {
+    for (Pair<HttpRequest, HttpResponse> event : requestTrace) {
+      if (event.two == null || event.two.isError()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private String getDetails() {
+    return "OAuth error [" + error + ", " + errorText + "] for application " +
+        securityToken.getAppUrl() + ".  Request trace:" + getRequestTrace();
+  }
+
+  private String getRequestTrace() {
+    StringBuilder trace = new StringBuilder();
+    trace.append("\n==== Original request:\n");
+    trace.append(originalRequest);
+    trace.append("\n====");
+    int i = 1;
+    for (Pair<HttpRequest, HttpResponse> event : requestTrace) {
+      trace.append("\n==== Sent request " + i + ":\n");
+      if (event.one != null) {
+        trace.append(filterSecrets(event.one.toString()));
+      }
+      trace.append("\n==== Received response " + i + ":\n");
+      if (event.two != null) {
+        trace.append(filterSecrets(event.two.toString()));
+      }
+      trace.append("\n====");
+      ++i;
+    }
+    return trace.toString();
+  }
+
+  /**
+   * Removes security sensitive parameters from requests and responses.
+   */
+  static String filterSecrets(String in) {
+    Matcher m = REMOVE_SECRETS.matcher(in);
+    return m.replaceAll("REMOVED");
+  }
+
+  /**
+   * Update a response with additional data to be returned to the application.
+   */
   public void addToResponse(HttpResponseBuilder response) {
     if (!newClientState.isEmpty()) {
       try {
@@ -71,13 +190,24 @@
       response.setMetadata(APPROVAL_URL, aznUrl);
     }
     if (error != null) {
-      response.setMetadata(ERROR_CODE, error.toString());
+      response.setMetadata(ERROR_CODE, error);
     }
-    if (errorText != null) {
-      response.setMetadata(ERROR_TEXT, errorText);
+    if (errorText != null || sendTraceToClient) {
+      StringBuilder verboseError = new StringBuilder();
+      if (errorText != null) {
+        verboseError.append(errorText);
+      }
+      if (sendTraceToClient) {
+        verboseError.append("\n");
+        verboseError.append(getRequestTrace());
+      }
+      response.setMetadata(ERROR_TEXT, verboseError.toString());
     }
   }
 
+  /**
+   * Get the state we will return to the client.
+   */
   public OAuthClientState getNewClientState() {
     return newClientState;
   }
@@ -86,24 +216,71 @@
     return aznUrl;
   }
 
+  /**
+   * Set the authorization URL we will return to the client.
+   */
   public void setAznUrl(String aznUrl) {
     this.aznUrl = aznUrl;
   }
 
-  public OAuthError getError() {
+  public boolean sendTraceToClient() {
+    return sendTraceToClient;
+  }
+
+  public void setSendTraceToClient(boolean sendTraceToClient) {
+    this.sendTraceToClient = sendTraceToClient;
+  }
+
+  public String getError() {
     return error;
   }
 
-  public void setError(OAuthError error) {
-    this.error = error;
+  public OAuthRequestException oauthRequestException(OAuthError error, String errorText) {
+    return oauthRequestException(error.toString(), errorText);
+  }
+
+  public OAuthRequestException oauthRequestException(OAuthError error, String errorText, 
+      Throwable cause) {
+    return oauthRequestException(error.toString(), errorText, cause);
   }
 
-  public String getErrorText() {
-    return errorText;
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   */
+  public OAuthRequestException oauthRequestException(String error, String errorText) {
+    Check.notNull(error);
+    Check.notNull(errorText);
+    this.error = error;
+    this.errorText = errorText;
+    return new OAuthRequestException("[" + error + "," + errorText + "]");
   }
 
-  public void setErrorText(String errorText) {
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   */
+  public OAuthRequestException oauthRequestException(String error, String errorText,
+      Throwable cause) {
+    Check.notNull(error);
+    Check.notNull(errorText);
+    this.error = error;
     this.errorText = errorText;
+    return new OAuthRequestException("[" + error + "," + errorText + "]", cause);
+  }
+
+  /**
+   * Superclass for all exceptions thrown from OAuthRequest and friends.
+   * 
+   * The constructors are private, use OAuthResponseParams.oauthRequestException to create this
+   * exception.  This makes sure that any exception thrown is also exposed to the calling gadget
+   * in a useful way.
+   */
+  public class OAuthRequestException extends Exception {
+    private OAuthRequestException(String message) {
+      super(message);
+    }
+
+    private OAuthRequestException(String message, Throwable cause) {
+      super(message, cause);
+    }
   }
-  
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java?rev=733188&r1=733187&r2=733188&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java Fri Jan  9 15:33:46 2009
@@ -23,7 +23,6 @@
 import net.oauth.OAuthAccessor;
 import net.oauth.OAuthException;
 import net.oauth.OAuthMessage;
-import net.oauth.OAuthProblemException;
 import net.oauth.OAuth.Parameter;
 
 import java.io.IOException;
@@ -53,16 +52,7 @@
       throw new RuntimeException(e);
     }
   }
-  
-  public static void requireParameters(OAuthMessage message, String... names)
-      throws OAuthProblemException {
-    try {
-      message.requireParameters(names);
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-  
+
   public static String formEncode(Iterable<? extends Entry<String, String>> parameters) {
     try {
       return OAuth.formEncode(parameters);
@@ -70,7 +60,7 @@
       throw new RuntimeException(e);
     }
   }
-  
+
   public static String addParameters(String url, List<Entry<String, String>> parameters) {
     try {
       return OAuth.addParameters(url, parameters);
@@ -78,7 +68,7 @@
       throw new RuntimeException(e);
     }
   }
-  
+
   public static OAuthMessage newRequestMessage(OAuthAccessor accessor, String method, String url,
       List<Parameter> parameters) throws OAuthException {
     try {